0%

Concurrency Programming Guide:操作队列

Cocoa操作对象是一种以面向对象的方式来封你需要异步执行的任务。操作对象被设计成跟操作队列队列一起使用,或者单独使用。因为是基于Objective-C实现的,操作对象可同时在 OS X 和 iOS 中使用。

关于操作对象

一个操作对象是一个 NSOperation 类的实例,用其封装需要执行的任务。NSOperation 是抽象基类,如果你要做啥具体的任务,必须要通过其子类来完成。尽管是抽象基类,NSOperation 仍然提供了重要的基础设施,以减少子类的工作量。另外,Foundation框架提供了两个具体的子类供开发者直接使用。

描述
NSInvocationOperation 该类可以直接使用,通过程序对象和selector直接创建一个操作对象。你可以对已有的任务方法使用该类。因为其不需要子类化,所以也可以用该类以更动态的方式创建操作对象。关于如何使用该类的更多信息,可参阅Creating an NSInvocationOperation Object
NSBlockOperation 该类可以直接使用,可以执行一到多个block。因为其可以执行一到多个block,所以该操作对象使用组的语义进行操作,只有当相关的block都执行完毕时,操作对象本身才算完成。关于使用该类的更多信息,可参阅Creating an NSBlockOperation Object
NSOperation 该类是用于自定义操作对象的基类。通过子类化NSOperation,你可以完全控制自己的操作实现,包括改变操作执行和汇报状态的默认方式。关于如何自定义操作对象的更多信息,可参阅自定义操作对象

所有操作对象都支持以下特性:

操作对象被设计来提高应用的并发水平。操作对象也是组织和封装程序行为到简单离散块的方式。你可以把一到多个操作提交到一个队列,让相应的工作在一到多个单独的线程上异步执行,而不是全都集中在程序主线程上执行。

并发与非并发操作对象

尽管通常把操作添加操作队列来执行,但这样做不是必须的。也可以直接手动调用start来执行一个操作对象,但这样做并不能保证与其他代码并行执行。NSOperation类的isConcurrent告诉你该操作对象相对于调用 start 方法的线程是否是异步的。默认返回 NO,表示操作对象同步地跑在调用的线程上。

如果你需要实现一个并发的操作对象,也就是说,相对于调用线程而言是异步执行的,你必须写额外的代码异步的启动操作对象。例如,你可能创建一个线程,调用异步系统函数,或者任何保证 start 方法启动任务,并立即返回,而且很有可能在任务完成之前返回。

大部分开发者应该绝不需要实现并发操作对象。如果你总是将操作对象添加到队列中,你不需要实现并发操作对象。当你提交一个非并发操作对象到操作队列的时候,队列自身会创建一个执行操作对象的线程。因此,添加一个非并发操作对象到操作队列仍然导致了操作对象的异步执行。你应只在需要异步执行操作对象但又不添加到操作队列的情况下才定义并发操作对象。

关于如何创建一个并发操作对象,可参阅Configuring Operations for Concurrent ExecutionNSOperation Class Reference

创建NSInvocationOperation对象

NSInvocationOperation 类是 NSOperation 的具体子类,运行时调用你指定对象的selector。使用这个类可以减少大量的自定义操作对象的需求,尤其是修改程序已实现的对象和任务方法时。当你希望调用的方法可以修改时也可以使用该类。例如,你可以使用一个调用操作来执行一个基于用户输入动态选择的selector。

创建invocation操作对象很简单。创建并初始化该类的实例,把需要执行的对象和selector传递给初始化方法。清单2-1展示了一个自定义类的两个方法,演示了创建过程:

清单2-1 创建一个NSInvocationOperation对象

1
2
3
4
5
6
7
8
9
10
11
12
13
@implementation MyCustomClass
- (NSOperation*)taskWithData:(id)data {
NSInvocationOperation* theOp = [[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(myTaskMethod:) object:data];

return theOp;
}

// This is the method that does the actual work of the task.
- (void)myTaskMethod:(id)data {
// Perform the task.
}
@end

创建NSBlockOperation对象

NSBlockOperation 同样是 NSOperation 的具体子类,用来封装一个或多个block。这个类给那些已经使用了操作队列并不想创调度发队列的应用提供了面向对象的封装。通过操作队列可以使用那些调度队列没有的一些特性,如操作对象依赖、KVO通知等。

当你创建一个block操作对象的时,通常在初始化的时候你添加一个 block;后续你还可以添加多个block。当执行一个 NSBlockOperation 对象的时候,该对象会把所有的 block提交给默认优先级的并发调度队列上。对象会等待所有的 block 执行完毕。当最后的一个 block 执行完后,对象会置自己的状态为完成。因此,你可以使用一个 block操作对象来追踪一组执行的 block,就像使用一个线程 join merge 多个线程执行的结果。因为 block操作对象跑在独立的线程上,程序的其他线程中的任务不受影响,同时可以等待 block操作对象的完成。

清单2-2显示了一个如何创建NSBlockOperation对象的简单示例。该block本身没有参数,也没有重要的返回结果。

清单2-2 创建一个NSBlockOperation对象

1
2
3
4
NSBlockOperation* theOp = [NSBlockOperation blockOperationWithBlock: ^{
NSLog(@"Beginning operation.\n");
// Do some work.
}];

创建了 block 操作对象之后,你可以使用 addExecutionBlock: 方法添加更多的 block。如果你需要串行执行 block,你必须直接把 block 提交给指定的调度队列。

自定义操作对象

如果 block 操作对象和 invocation 操作对象都不能满足程序的需求,你可以直接实现 NSOperation 的子类,添加需要的行为。NSOperation 类对所有操作对象提供了通用的子类,也提供了大量的基础设施来处理依赖管理和 KVO 通知。然而,仍然有些时候你需要补充先有的基础设施以确保操作行为的正确。要做的额外工作量取决于你在实现一个非并发还是并发操作对象。

定义一个非并发操作比并发操作简单得多。对于非并发操作对象而言,所有你需要做的是 main task 和合理的响应取消事件;已经存在的基础设施已经为你完成了其他工作。对于一个并发操作对象而言,你必须使用自定义的代码替换掉现有的基础设施。下来的部分将要说明怎么实现这两种类型。

执行Main Task

每个操作对象至少实现以下方法:

  • 一个自定义的初始化方法
  • main方法

你需要一个自定义的初始化方法将你的操作对象放入已知的状态,一个 main 方法来执行的你的任务。当然可以根据需要实现额外的方法,如下:

  • 打算从 main 方法调用的自定义方法
  • 设置数据和获取结果的属性访问器
  • NSCoding中的归档和解档方法

下例展示了一个自定义 NSOperation 的启动模板(代码中没有展示在怎么处理 cancellation,但展示了你通常需要的方法)。

清单2-3展示了一个自定义NSOperation子类的初始模板。(这个清单没有显示如何处理取消,但显示了你通常实现的方法。关于处理取消,可参阅Responding to Cancellation Events)。) 该类的初始化方法需要一个单一的对象作为数据参数,并在操作对象中存储对它的引用。main方法表面上是对该数据对象进行处理,然后将结果返回给程序。

清单2-3 定义一个简单的操作对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface MyNonConcurrentOperation : NSOperation
@property id (strong) myData;
-(id)initWithData:(id)data;
@end

@implementation MyNonConcurrentOperation
- (id)initWithData:(id)data {
if (self = [super init])
myData = data;
return self;
}

-(void)main {
@try {
// Do some work on myData and report the results.
}
@catch(...) {
// Do not rethrow exceptions.
}
}
@end

响应取消事件

在操作对象开始执行后,它要么持续执行到任务完成,要么被显式取消。取消可以发生在任何时候,甚至在操作对象开始执行之前。尽管 NSOperation 类给用户提供了一种方式来取消一个操作对象,但是否识别取消事件是还是开发者决定的。如果一个操作对象被错误地停止了,可能就没有办法回收已经分配的资源。所以,操作对象应该在执行的过程中检查取消事件,并在操作过程中发生取消事件时优雅地退出。

为了支持操作对象的取消,你所要做的就是定期从你的自定义代码中调用对象的isCancelled方法,如果它返回YES就立即返回。无论你的操作持续时间长短,也无论你是直接对NSOperation进行子类化还是使用其具体的子类,支持取消都很重要。isCancelled方法本身是非常轻量的,可以在不影响性能的情况下频繁调用。当设计你的操作对象时,你应考虑在代码中的以下地方调用isCancelled方法:

  • 在执行任何实际工作前立即调用;
  • 每次循环迭代至少调用一次,如果单次循环确实很长的话,可以多次检查;
  • 在代码中任何一个相对容易中止操作的地方;

清单2-4展示了一个非常简单的示例,说明如何在一个操作对象的main方法中响应取消事件。在这种情况下,每次通过while循环调用isCancelled方法,允许在工作开始前快速退出,并以一定的间隔再次退出。

1
2
3
4
5
6
7
8
9
10
11
12
- (void)main {
@try {
BOOL isDone = NO;

while (![self isCancelled] && !isDone) {
// Do some work and set isDone to YES when finished
}
}
@catch(...) {
// Do not rethrow exceptions.
}
}

尽管上述代码中没有包含清理资源的代码,但是你自己的代码中应该清理任何你分配的资源。

为并发执行配置操作对象

操作对象默认以同步方式执行,也就是说,它们在调用其start方法的线程中执行任务。因为操作队列为非并发操作提供了线程,尽管如此,大多数操作仍然以异步方式运行。然而,如果你打算手动执行操作,并且仍然希望它们异步运行,你就可以通过把操作对象定义为一个并发操作来达到目的。

下表列出了在实现并发操作对象时需要 override 的方法:

方法 描述
start 必须 所有的并发操作都必须覆盖这个方法,用自定义实现替换默认行为。要手动执行一个操作,要调用其start方法。因此,你对这个方法的实现是你的操作的起点,是你设置线程或其他执行环境来执行你的任务的地方。自定义实现在任何时候都不能调用super方法。
main 可选 这个方法通常用于实现与操作对象相关的任务。尽管你可以在start方法中执行任务,但使用这个方法实现任务可以使你的设置和任务代码更清晰地分开。
isExecuting
isFinished
必须 并发操作负责设置其执行环境并向外部客户报告该环境的状态。因此,一个并发操作必须维护一些状态信息,以知道它何时在执行任务,何时完成了该任务。然后,它必须使用这些方法汇报该状态。
对这些方法的实现必须是安全的,可以从其他线程同时调用。当改变这些方法所汇报的值时,你还必须为预期的key path生成适当的KVO通知。
isConcurrent 必须 要确定操作对象是一个并发操作,覆盖这个方法并返回YES

这节剩余的部分展示 MyOperation 类的实现示例,展示了实现一个并发操作所需的基本代码。 MyOperation 只是简单在它创建的线程上执行 main 方法。main 方法的具体内容在这里是不相关的。示例的意义在于展示在定义一个并发操作时需要提供的基础设施。

清单2-5显示了MyOperation类的接口和部分实现。MyOperation类的isConcurrentisExecutingisFinished方法的实现相对简单。isConcurrent方法应该简单地返回YES,表示这是一个并发操作。isExecutingisFinished方法只是返回存储在类本身的实例变量中的值。

清单2-5 定义一个并发操作队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@interface MyOperation : NSOperation {
BOOL executing;
BOOL finished;
}
- (void)completeOperation;
@end

@implementation MyOperation
- (id)init {
self = [super init];
if (self) {
executing = NO;
finished = NO;
}
return self;
}

- (BOOL)isConcurrent {
return YES;
}

- (BOOL)isExecuting {
return executing;
}

- (BOOL)isFinished {
return finished;
}
@end

清单2-6显示了MyOperationstart方法。这个方法的实现是最小的,以便展示你绝对必须执行的任务。在这种情况下,该方法只是启动了一个新的线程,并配置它来调用main方法。该方法还更新了executing成员变量,并为isExecuting key path生成KVO通知,以反映该值的变化。完成工作后,这个方法就简单地返回,让新分离的线程来执行实际的任务。

清单2-6 start方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)start {
// Always check for cancellation before launching the task.
if ([self isCancelled])
{
// Must move the operation to the finished state if it is canceled.
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}

// If the operation is not canceled, begin executing the task.
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}

清单2-7显示了MyOperation类的其余实现。正如在清单2-6中看到的,main方法是一个新线程的入口。它执行与操作对象相关的工作,并在工作最终完成时调用自定义的completeOperation方法。然后completeOperation方法为isExecutingisFinished key path生成所需的KVO通知,以反映操作状态的变化。

清单2-7 在完成时更新操作对象状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)main {
@try {

// Do the main work of the operation here.

[self completeOperation];
}
@catch(...) {
// Do not rethrow exceptions.
}
}

- (void)completeOperation {
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];

executing = NO;
finished = YES;

[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}

即使一个操作被取消了,你也应始终通知KVO观察者你的操作现在已经完成了它的工作。当一个操作对象依赖于其他操作对象的完成时,它将监听这些对象的isFinished key path。只有当所有对象都汇报已经完成时,依赖的操作才会发出信号说它已经准备好运行。因此,未能生成一个完成通知可能会阻止程序中其他操作对象的执行。

维护KVO

NSOperation 类对以下 key path 是 KVO 的:

  • isCancelled
  • isConcurrent
  • isExecuting
  • isFinished
  • isReady
  • dependencies
  • queuePriority
  • completionBlock

如果你覆盖了 start 方法或大幅度的自定义一个 NSOperation 对象,而不是覆盖 main 方法,你需确保自定义对象仍然保持着这些 key path 的 KVO 兼容性。当你覆盖 start 方法的时候,你应该关心的 key path 是 isExecutingisFinished。这些 key paths 是重写 start 方法最常影响到的。

如果你想实现对其他操作对象以外的依赖关系的支持,你也可以覆盖isReady方法并强制它返回NO直到你的自定义依赖关系得到满足。(如果你实现了自定义的依赖关系,如果你仍然支持由NSOperation类提供的默认依赖关系管理系统,确保从isReady方法中调用super)。当操作对象的准备状态发生变化时,为isReady key path生成KVO通知以报告这些变化。除非你覆盖了addDependency:removeDependency:方法,否则你不需要担心为dependencies关键路径产生KVO通知。

尽管你可以为NSOperation的其他key path生成KVO通知,但你不太可能需要这样做。如果你需要取消一个操作,你可以简单地调用现有的cancel方法来完成。同样地,你应该很少需要修改操作对象中的队列优先级信息。最后,除非你的操作能够动态地改变其并发状态,否则你不需要为isConcurrent key path提供KVO通知。

自定义操作对象执行行为

操作对象的配置发生在创建之后,添加到队列之前。本节描述的配置类型可用于所有的操作对象,无论是对NSOperation进行子类化还是使用现有的NSOperation子类。

配置交互依赖

依赖可以串行不同操作对象。依赖其他操作对象的操作对象在其他操作对象完成之前不能开始执行。因此,你可以使用依赖在两个操作对象之间建立简单的一对一的依赖关系,或者建立复杂的对象依赖关系图。

使用NSOperationaddDependency:方法可以创建依赖关系。这个方法创建单向的依赖关系,当前操作对象依赖于参数给定的操作对象。依赖不限于同一个队列的操作对象。操作对象管理着它们自己的依赖,所以它不受队列局限,但不能创建在操作之间创建循环依赖关系。这是一个开发者的错误,会导致受影响的操作永远无法执行。

当一个操作对象的所有依赖都结束执行时,通常该操作变成准备执行中状态。(如果你自定义了 isReady 方法的话,操作对象的就绪状态就由你自定义行为决定了)。如果操作对象在队列中,队列可能随时启动执行其中的操作对象。否则如果你想手动执行该操作,则由你来调用该操作的start方法。

重要提醒:你应总是在运行操作对象或将其添加到操作队列之前配置依赖关系。在这之后添加的依赖关系可能不会阻止某个操作对象的运行。

依赖机制依赖于每个操作对象在对象的状态发生变化时发送适当的KVO通知。如果你自定义了操作对象的行为,你可能需要从你自定义代码中生成适当的KVO通知,以避免引起依赖关系的问题。关于KVO通知和操作对象的更多信息,可参阅维护KVO。关于配置依赖关系的其他信息,可参阅NSOperation Class Reference

修改操作对象执行的优先级

对于添加到队列中的操作对象,执行顺序首先由队列中的操作的准备状态决定,然后由其相对优先级决定。准备状态由一个操作对象对其他操作对象的依赖决定,但优先级是操作对象本身的一个属性。默认情况下,所有新的操作对象都有一个normal的优先级,你可以调用操作对象的setQueuePriority:方法来增加或减少优先级。

优先级只适用于同一操作队列中的操作操作对象。如果程序有多个操作队列,每个队列都会独立于其他队列来确定自己操作的优先级。因此,低优先级的操作仍有可能在不同队列的高优先级操作之前执行。

优先级不能替代依赖关系。优先级只是决定了操作队列中的那些处于就绪状态的操作对象的执行顺序。例如,如果一个队列同时包含高优先级和低优先级的操作,并且这两个操作都就绪了,那么这个队列会先执行高优先级的操作。但是,如果高优先级的操作对象还没就绪,而低优先级的操作对象已经就绪了,那么队列就会先执行低优先级的操作对象。如果你想阻止一个操作在另一个操作完成之前开始,你必须使用依赖实现。

修改底层线程的优先级

在OS X v10.6及以后的版本中,可以配置操作对象的底层线程的执行优先级。系统中的线程策略本身由内核管理,但一般来说,高优先级的线程比低优先级的线程有更多机会运行。在一个操作对象中,可以把线程优先级设置为0.0到1.0范围内的浮点值,0.0为最低优先级,1.0为最高优先级。如果没有设置一个明确的线程优先级,操作对象将以默认的线程优先级0.5运行。

要设置操作对象的线程优先级,必须在操作对象添加到队列(或手动执行)之前调用操作对象的setThreadPriority:方法。当执行操作的时候,默认的start方法使用你指定的值来修改当前线程的优先级。这个新的优先级只在操作对象的main方法期间保持有效。所有其他代码(包括操作对象的完成block)都以默认的线程优先级运行。如果你创建了一个并发的操作对象,并因此覆盖了start方法,你必须自己配置线程优先级。

设置完成Block

在OS X v10.6和更高版本中,当一个操作对象的主任务执行完毕时,可以执行一个完成block。你可以使用一个完成block来执行任何你认为不属于主任务的工作。例如,你可以使用这个block来通知感兴趣的对象,操作对象本身已经完成。一个并发的操作对象可能会使用这个block来生成其最终的KVO通知。

要设置完成block,使用NSOperationsetCompletionBlock:方法。该block没有参数也没有返回值。

实现操作对象的技巧

尽管操作对象的实现相当容易,但在编写代码时,有几件事你应该注意。下面几节描述了在编写操作对象的代码时应该考虑的一些因素。

在操作对象中管理内存

下面的章节描述了操作对象中内存管理的关键。关于Objective-C程序中内存管理的一般信息,可参阅Advanced Memory Management Programming Guide

避免按线程存储

尽管大多数操对象作是在一个线程上执行的,但在非并发操作对象的情况下,这个线程通常是由一个操作队列提供的。如果一个操作队列为你提供了一个线程,你应该认为这个线程是由队列所持有的,而不会被你的操作对象所访问。具体来说,你不应该将任何数据与非自己创建或管理的线程联系起来。由操作队列管理的线程会根据系统和程序的需要而创建和销毁。因此,使用按线程存储在操作之间传递数据是不可靠的,很可能会失败。

就操作对象而言,无论在什么情况下都不应使用按线程存储。当初始化一个操作对象时,你应该为该对象提供它所需要的一切来完成其工作。因此,操作对象本身提供了你需要的上下文存储。所有传入和传出的数据都应该存储在操作对象中,直到它可以被整合回程序或不再需要的时候。

根据需要持有操作对象

仅仅因为操作对象是异步运行的,你不该只是简单地完成它的创建。它们仍只是个对象,你应管理好它的生命周期。如果你需要在一个操作完成后检索结果数据,保持对操作对象的引用尤其重要。

你应该始终保持对操作的引用,原因是你以后可能没有机会从队列获取到该对象。队列会尽一切努力尽可能快地调度和执行操作。在许多情况下,队列在添加操作对象后几乎立即开始执行操作。当你自己的代码回到队列中获取对操作对象的引用时,该操作可能已经完成并从队列中移除了。

处理错误和异常

因为操作对象本质上是程序中的离散实体,它们负责处理任何出现的错误或异常。在OS X v10.6及以后的版本中,NSOperation类提供的默认start方法并不捕捉异常。(在OS X v10.5中,start方法可以捕捉和抑制异常。)代码应该直接捕捉和抑制异常。它还应该检查错误代码,并根据需要通知到程序中合适的地方。如果替换了start方法,你必须在自定义实现中捕捉任何异常,以防止它们离开底层线程的作用域。

你应该处理以下类型的错误:

  • 检查和处理UNIX errno形式的error code。
  • 检查由方法和函数返回的显式error code。
  • 捕获由自己的代码或其他系统框架抛出的异常。
  • 捕捉由NSOperation类本身抛出的异常,在以下情况下它会抛出异常:
    • 当操作对象还没有就绪执行,但它的start方法被调用时;
    • 当操作对象正在执行或完成时(可能是因为它被取消了),而它的start方法被再次调用时;
    • 当你试图给一个已经执行或完成的操作对象添加一个完成block时;
    • 当你试图检索一个被取消的NSInvocationOperation对象的结果时;

如果自定义代码确实遇到了异常或错误,你应该采取任何必要的步骤将该错误传播到程序的其他位置。NSOperation类没有提供明确的方法来实现这部分工作。因此,如果这些信息对程序很重要,你必须提供必要的代码。

为操作对象确定合适的范围

尽管存在在一个操作队列中添加任意多操作的可能,但这样做往往是不切实际的。像任何对象一样,NSOperation类的实例会消耗内存,执行也有相应的开销。如果每个操作对象只做少量的工作,而你创建了数以万计的操作对象,你可能会发现花在调度操作对象上的时间比做真正的操作任务要多。如果程序已经受到了内存的限制,你可能会发现,仅仅在内存中拥有成千上万的操作对象可能会进一步降低性能。

有效使用操作对象的关键是在你需要在具体操作任务和保持计算机持续工作之间找到一个适当的平衡点。尽量确保操作对象完成合理的工作量。例如,如果程序创建了100个操作对象来对100个不同的值执行相同的任务,可以考虑改成创建10个操作对象,每个操作对象处理10个值。

你还应避免一次向队列中添加大量的操作对象,或者避免向队列中添加操作对象的速度超过它们的处理速度。与其一次性添加大量的操作对象,不如分批创建这些对象。当一个批次执行完毕后,使用一个完成block来告诉程序创建一个新的批次。这种方案适用于由大量的任务要进行,想让队列填充足够多的操作对象,来让计算机持续执行的情况。一次性创建大量的操作对象,让直接让程序耗尽内存。

当然,创建操作对象的数量,以及你在每个操作中执行的工作量是可变的,完全取决于你的程序。你应该总是使用诸如Instruments这样的工具来帮助你在效率和速度之间找到一个适当的平衡点。关于Instruments和其他性能工具的概述,可以用来为你的代码收集指标,可参阅Performance Overview

执行操作对象

最终,你的应用程序需要执行操作对象,以完成相关的工作。在本节中,将学习几种执行操作对象的方法,以及如何在运行时控制操作对象的执行行为。

添加操作对象到操作队列中

到目前为止,执行操作对象的最简单方法是使用一个操作队列,它是NSOperationQueue类的实例。程序负责创建和维护使用的任何操作队列。程序可以有任何数量的队列,但在一个给定的时间点上操作对象可以执行的数量是有实际限制的。操作队列与系统配合工作,将并发操作的数量限制在一个适合可用内核和系统负载的数值上。因此,创建更多的队列并不意味着你可以执行更多的操作对象。

创建队列跟创建其他的对象是一样的:

1
NSOperationQueue* aQueue = [[NSOperationQueue alloc] init];

要向队列添加操作,可以使用addOperation:方法。在OS X v10.6及更高的版本中,你可以使用addOperations:waitUntilFinished:方法添加操作组,或者使用addOperationWithBlock:方法直接向队列添加block对象(不会有相应的操作对象)。这些方法都是排队一个或多个操作对象,并通知队列应该开始处理这些操作对象。在大多数情况下,操作对象在被添加到队列后不久就会被执行,但是操作队列可能会因为一些原因而延迟执行队列中的操作。具体来说,如果排队的操作对象依赖于其他尚未完成的操作,执行可能会被延迟。如果操作队列本身被暂停或已经在执行其最大数量的并发操作,执行也可能被延迟。下面的例子显示了向队列添加操作对象的基本语法:

1
2
3
4
5
[aQueue addOperation:anOp]; // Add a single operation
[aQueue addOperations:anArrayOfOps waitUntilFinished:NO]; // Add multiple operations
[aQueue addOperationWithBlock:^{
/* Do something. */
}];

重要提醒:在将操作对象添加到队列之前,你应该对其完成所有必要的配置和修改,因为一旦添加,操作对象可能会在任何时候被运行,这可能会让修改的时间太晚,无法产生预期的效果。

虽然NSOperationQueue类是为操作对象的并发执行而设计的,但也可以强制队列一次只运行一个操作。setMaxConcurrentOperationCount:方法可以让你配置操作队列对象的最大并发操作对象数。给这个方法传递1,会使队列一次只执行一个操作。虽然一次只能执行一个操作对象,但执行的顺序仍然是基于其他因素,比如每个操作对象的就绪状态和分配的优先级。因此,一个串行的操作队列所提供的行为与Grand Central Dispatch中的串行调度队列不完全相同。如果操作对象的执行顺序对你很重要,你应该在把操作对象添加到队列之前,使用依赖来建立这个顺序。关于配置依赖关系的信息,可参阅配置交互依赖

关于使用操作队列的信息,可参阅NSOperationQueue Class Reference。关于串行调度队列的更多信息,可参阅Creating Serial Dispatch Queues

手动执行操作对象

虽然操作队列是运行操作对象的最方便的方式,但也可以不通过队列来执行操作对象。然而,如果你选择手动执行操作,你应该在你的代码中采取一些预防措施。特别是,操作必须准备好运行,你必须始终使用它的start方法来启动它。

一个操作在它的isReady方法返回YES时才被认为能够运行。isReady方法被集成到NSOperation类的依赖管理系统中,以提供操作的依赖关系的状态。只有当它的依赖关系被清除后,一个操作才可以自由地开始执行。

当手动执行一个操作时,你应该总是使用start方法来开始执行。而不是main或其他方法,因为start方法在实际运行自定义代码之前会执行一些安全检查。特别是,默认的start方法会生成操对象作所需的KVO通知,以正确处理其依赖关系。如果操作对象已经被取消了,这个方法也会正确地避免执行你的操作,如果操作对象实际上没有就绪运行,则会抛出一个异常。

如果你的程序定义了并发的操作对象,你也应该考虑在启动操作对象之前调用操作的isConcurrent方法。在该方法返回NO的情况下,本地代码可以决定是在当前线程中同步执行操作还是先创建一个单独的线程。然而,实现这种检查完全由你决定。

清单2-8显示了一个简单的示例,以说明手动执行操作之前应该进行什么样的检查。如果该方法返回NO,你可以安排一个定时器并在稍后再次调用该方法。然后你会不断地重新安排定时器,直到方法返回YES,这可能是因为操作被取消了。

清单2-8 手动执行一个操作对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (BOOL)performOperation:(NSOperation*)anOp
{
BOOL ranIt = NO;

if ([anOp isReady] && ![anOp isCancelled])
{
if (![anOp isConcurrent])
[anOp start];
else
[NSThread detachNewThreadSelector:@selector(start)
toTarget:anOp withObject:nil];
ranIt = YES;
}
else if ([anOp isCancelled])
{
// If it was canceled before it was started,
// move the operation to the finished state.
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];

// Set ranIt to YES to prevent the operation from
// being passed to this method again in the future.
ranIt = YES;
}
return ranIt;
}

取消操作对象

一旦被添加到一个操作队列中,操作对象就有效地被队列所拥有,并且不能被移除。取消一个操作对象的唯一方法是取消它。你可以通过调用一个单独的操作对象的cancel方法来取消它,或者通过调用队列对象的cancelAllOperations方法来取消队列中的所有操作对象。

只有当你确定不再需要这些操作对象时,才取消它们。发出取消命令会使操作对象进入canceled状态,这将使它永远无法运行。因为一个被取消的操作仍然被认为是finished的,依赖于它的对象会收到适当的KVO通知来清除这种依赖关系。因此,更常见的情况是,在某些重要事件中取消所有排队的操作对象,比如程序退出或用户特别要求取消,而不是选择性地取消某个操作对象。

等待操作对象完成

为了获得最佳性能,你应该把操作对象设计成尽可能的异步,让程序在操作对象执行时可以自由地做其他工作。如果创建一个操作对象的代码也处理该对象的结果,你可以使用NSOperationwaitUntilFinished方法来阻塞代码,直到操作完成。不过一般来说,如果可以的话,最好避免调用这个方法。阻塞当前线程可能是一个方便的解决方案,但它给你的代码引入了更多的串行,并限制了整体的并发水平。

重要提醒:你不应该在程序的主线程中等待一个操作。你只应该从子线程或其他操作对象中进行等待。阻塞你的主线程会阻止程序对用户事件做出响应,并可能使程序看起来没有反应。

除了等待单个操作完成,你还可以通过调用NSOperationQueuewaitUntilAllOperationsAreFinished方法来等待一个队列中的所有操作对象的完成。当等待整个队列完成时,要注意程序的其他线程仍然可以向队列添加操作,但因此也会延长等待时间。

暂停和恢复队列

如果要暂停操作对象的执行,你可以使用setSuspended:方法暂停相应的操作队列。暂停一个队列并不会导致已经执行的操作对象在其任务中暂停。它只是阻止队列安排新的操作对象执行。你可以暂停一个队列,以响应用户的请求,暂停任何正在进行的工作,因为预期用户最终可能想要恢复该对队列工作。

总结

  • NSBlockOperation可以添加多个block,该操作对象使用组的语义进行操作,只有当相关的block都执行完毕时,操作对象本身才算完成。
  • 对于单个block中的代码来说,其执行都是同步的。
  • 操作对象的任务要自行处理异常。
  • 操作对象的配置发生在创建之后,添加到队列之前。
  • 若要保持对操作对象的检索,最好自己添加对操作对象的引用。
  • 要手动执行操作对象,则执行start方法。
  • 取消往往用于对队列的行为,而非个别操作对象。

使用技巧:

  • 应只在需要单独异步执行操作对象但又不添加到操作队列的情况下才定义并发操作对象。
  • 操作对象的执行顺序主要是基于依赖建立的。不建议通过优先级改变操作对象执行顺序。
  • 操作队列可以通过setMaxConcurrentOperationCount:方法设置并发数量,即使设置为1,其行为也与串行调度队列不完全一致。例如,经测试,操作队列即使并发限制为1,单每次使用的线程可能不同。
  • 操作对象的完成block执行的语义应是不属于主任务的工作。

欢迎关注我的其它发布渠道