iOS Concurrency Programming Guide
iOS 和 Mac OS 传统的并发编程模型是线程,不过线程模型伸缩性不强,而且编写正确的线程代码也不容易。Mac OS 和 iOS 采取 asynchronous design approach 来解决并发的问题。
引入的异步技术有两个:
Grand Central Dispatch:系统管理线程,你不需要编写线程代码。只需定义想要执行的任务,然后添加到适当的dispatch queue。Grand Central Dispatch会负责创建线程和调度你的任务。系统直接提供线程管理,比应用实现更加高效。
Operation Queue:Objective-C对象,类似于dispatch queue。你定义想要执行的任务,并添加任务到operation queue,后者负责调度和执行这些任务。和Grand Central Dispatch一样,Operation Queue也管理了线程,更加高效。
Dispatch Queue
基于C的执行自定义任务机制。dispatch queue按先进先出的顺序,串行或并发地执行任务。serial dispaptch queue一次只能执行一个任务,直接当前任务完成才开始出列并启动下一个任务。而concurrent dispatch queue则尽可能多地启动任务并发执行。
优点:
直观而简单的编程接口
提供自动和整体的线程池管理
提供汇编级调优的速度
更加高效地使用内存
不会trap内核under load
异步分派任务到dispatch queue不会导致queue死锁
伸缩性强
serial dispatch queue比锁和其它同步原语更加高效
Dispatch Sources
Dispatch Sources 是基于C的系统事件异步处理机制。一个Dispatch Source封装了一个特定类型的系统事件,当事件发生时提交一个特定的block对象或函数到dispatch queue。你可以使用Dispatch Sources监控以下类型的系统事件:
定时器
信号处理器
描述符相关的事件
进程相关的事件
Mach port事件
你触发的自定义事件
Operation Queues
Operation Queues是Cocoa版本的并发dispatch queue,由 NSOperationQueue 类实现。dispatch queue总是按先进先出的顺序执行任务,而 Operation Queues 在确定任务执行顺序时,还会考虑其它因素。最主要的一个因素是指定任务是否依赖于另一个任务的完成。你在定义任务时配置依赖性,从而创建复杂的任务执行顺序图
提交到Operation Queues的任务必须是 NSOperation 对象,operation object封装了你要执行的工作,以及所需的所有数据。由于 NSOperation 是一个抽象基类,通常你需要定义自定义子类来执行任务。不过Foundation framework自带了一些具体子类,你可以创建并执行相关的任务。
Operation objects会产生key-value observing(KVO)通知,对于监控任务的进程非常有用。虽然operation queue总是并发地执行任务,你可以使用依赖,在需要时确保顺序执行
异步设计技术
通过确保主线程自由响应用户事件,并发可以很好地提高应用的响应性。通过将工作分配到多核,还能提高应用处理的性能。但是并发也带来一定的额外开销,并且使代码更加复杂,更难编写和调试代码。
因此在应用设计阶段,就应该考虑并发,设计应用需要执行的任务,及任务所需的数据结构。
Operation Queues
基于Objective-C,因此基于Cocoa的应用通常会使用Operation Queues
Operation Objects
operation object 是 NSOperation 类的实例,封装了应用需要执行的任务,和执行任务所需的数据。NSOperation 本身是抽象基类,我们必须实现子类。Foundation framework提供了两个具体子类,你可以直接使用:
类 | 描述 |
NSInvocationOperation | 可以直接使用的类,基于应用的一个对象和selector来创建operation object。如果你已经有现有的方法来执行需要的任务,就可以使用这个类。 |
NSBlockOperation | 可以直接使用的类,用来并发地执行一个或多个block对象。operation object使用“组”的语义来执行多个block对象,所有相关的block都执行完成之后,operation object才算完成。 |
NSOperation | 基类,用来自定义子类operation object。继承NSOperation可以完全控制operation object的实现,包括修改操作执行和状态报告的方式。 |
所有operation objects都支持以下关键特性:
支持建立基于图的operation objects依赖。可以阻止某个operation运行,直到它依赖的所有operation都已经完成。
支持可选的completion block,在operation的主任务完成后调用。
支持应用使用KVO通知来监控operation的执行状态。
支持operation优先级,从而影响相对的执行顺序
支持取消,允许你中止正在执行的任务
并发 VS 非并发Operations
通常我们通过将operation添加到operation queue中来执行该操作。但是我们也可以手动调用start方法来执行一个operation对象,这样做不保证operation会并发执行。NSOperation类对象的 isConcurrent 方法告诉你这个operation相对于调用start方法的线程,是同步还是异步执行的。isConcurrent 方法默认返回NO,表示operation与调用线程同步执行。
如果你需要实现并发operation,也就是相对调用线程异步执行的操作。你必须添加额外的代码,来异步地启动操作。例如生成一个线程、调用异步系统函数,以确保start方法启动任务,并立即返回。
多数开发者从来都不需要实现并发operation对象,我们只需要将operations添加到operation queue。当你提交非并发operation到operation queue时,queue会创建线程来运行你的操作,因此也能达到异步执行的目的。只有你不希望使用operation queue来执行operation时,才需要定义并发operations。
创建一个 NSInvocationOperation 对象
如果已经现有一个方法,需要并发地执行,就可以直接创建 NSInvocationOperation 对象,而不需要自己继承 NSOperation。
- @implementation MyCustomClass
- - (NSOperation*)taskWithData:(id)data {
- NSInvocationOperation* theOp = [[[NSInvocationOperation alloc] initWithTarget:self
- selector:@selector(myTaskMethod:) object:data] autorelease];
- return theOp;
- }
- // This is the method that does the actual work of the task.
- - (void)myTaskMethod:(id)data {
- // Perform the task.
- }
- @end
创建一个 NSBlockOperation 对象
NSBlockOperation 对象用于封装一个或多个block对象,一般创建时会添加至少一个block,然后再根据需要添加更多的block。当 NSBlockOperation 对象执行时,会把所有block提交到默认优先级的并发dispatch queue。然后 NSBlockOperation 对象等待所有block完成执行,最后标记自己已完成。因此可以使用block operation来跟踪一组执行中的block,有点类似于thread join等待多个线程的结果。区别在于block operation本身也运行在一个单独的线程,应用的其它线程在等待block operation完成时可以继续工作。
- NSBlockOperation* theOp = [NSBlockOperation blockOperationWithBlock: ^{
- NSLog(@"Beginning operation.\n");
- // Do some work.
- }];
使用 addExecutionBlock: 可以添加更多block到这个block operation对象。如果需要顺序地执行block,你必须直接提交到所需的dispatch queue。
自定义Operation对象
如果block operation和invocation operation对象不符合应用的需求,你可以直接继承 NSOperation,并添加任何你想要的行为。NSOperation 类提供通用的子类继承点,而且实现了许多重要的基础设施来处理依赖和KVO通知。继承所需的工作量主要取决于你要实现非并发还是并发的operation。
定义非并发operation要简单许多,只需要执行主任务,并正确地响应取消事件;NSOperation 处理了其它所有事情。对于并发operation,你必须替换某些现有的基础设施代码。
执行主任务
每个operation对象至少需要实现以下方法:
自定义initialization方法:初始化,将operation 对象设置为已知状态
自定义main方法:执行你的任务
你也可以选择性地实现以下方法:
main方法中需要调用的其它自定义方法
Accessor方法:设置和访问operation对象的数据
dealloc方法:清理operation对象分配的所有内存
NSCoding 协议的方法:允许operation对象archive和unarchive
- @interface MyNonConcurrentOperation : NSOperation {
- id myData;
- }
- -(id)initWithData:(id)data;
- @end
- @implementation MyNonConcurrentOperation
- - (id)initWithData:(id)data {
- if (self = [super init])
- myData = [data retain];
- return self;
- }
- - (void)dealloc {
- [myData release];
- [super dealloc];
- }
- -(void)main {
- @try {
- NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
- // Do some work on myData and report the results.
- [pool release];
- }
- @catch(...) {
- // Do not rethrow exceptions.
- }
- }
- @end
响应取消事件
operation开始执行之后,会一直执行任务直到完成,或者显式地取消操作。取消可能在任何时候发生,甚至在operation执行之前。尽管 NSOperation 提供了一个方法,让应用取消一个操作,但是识别出取消事件则是你的事情。如果operation直接终止,可能无法回收所有已分配的内存或资源。因此operation对象需要检测取消事件,并优雅地退出执行。
operation 对象定期地调用 isCancelled 方法,如果返回YES(表示已取消),则立即退出执行。不管是自定义 NSOperation 子类,还是使用系统提供的两个具体子类,都需要支持取消。isCancelled方法本身非常轻量,可以频繁地调用而不产生大的性能损失。以下地方可能需要调用isCancelled:
在执行任何实际的工作之前
在循环的每次迭代过程中,如果每个迭代相对较长可能需要调用多次
代码中相对比较容易中止操作的任何地方
- - (void)main {
- @try {
- NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
- BOOL isDone = NO;
- while (![self isCancelled] && !isDone) {
- // Do some work and set isDone to YES when finished
- }
- [pool release];
- }
- @catch(...) {
- // Do not rethrow exceptions.
- }
- }
注意你的代码还需要完成所有相关的资源清理工作
为并发执行配置operations
Operation对象默认按同步方式执行,也就是在调用start方法的那个线程中直接执行。由于operation queue为非并发operation提供了线程支持,对应用来说,多数operations仍然是异步执行的。但是如果你希望手工执行operations,而且仍然希望能够异步执行操作,你就必须采取适当的措施,通过定义operation对象为并发操作来实现。
方法 | 描述 |
start | (必须)所有并发操作都必须覆盖这个方法,以自定义的实现替换默认行为。手动执行一个操作时,你会调用start方法。因此你对这个方法的实现是操作的起点,设置一个线程或其它执行环境,来执行你的任务。你的实现在任何时候都绝对不能调用super。 |
main | (可选)这个方法通常用来实现operation对象相关联的任务。尽管你可以在start方法中执行任务,使用main来实现任务可以让你的代码更加清晰地分离设置和任务代码 |
isExecuting isFinished | (必须)并发操作负责设置自己的执行环境,并向外部client报告执行环境的状态。因此并发操作必须维护某些状态信息,以知道是否正在执行任务,是否已经完成任务。使用这两个方法报告自己的状态。 这两个方法的实现必须能够在其它多个线程中同时调用。另外这些方法报告的状态变化时,还需要为相应的key path产生适当的KVO通知。 |
isConcurrent | (必须)标识一个操作是否并发operation,覆盖这个方法并返回YES |
- @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;
- }
- - (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"];
- }
- - (void)main {
- @try {
- NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
- // Do the main work of the operation here.
- [self completeOperation];
- [pool release];
- }
- @catch(...) {
- // Do not rethrow exceptions.
- }
- }
- - (void)completeOperation {
- [self willChangeValueForKey:@"isFinished"];
- [self willChangeValueForKey:@"isExecuting"];
- executing = NO;
- finished = YES;
- [self didChangeValueForKey:@"isExecuting"];
- [self didChangeValueForKey:@"isFinished"];
- }
- @end
即使操作被取消,你也应该通知KVO observers,你的操作已经完成。当某个operation对象依赖于另一个operation对象的完成时,它会监测后者的isFinished key path。只有所有依赖的对象都报告已经完成,第一个operation对象才会开始运行。如果你的operation对象没有产生完成通知,就会阻止其它依赖于你的operation对象运行。
维护KVO依从
NSOperation类的key-value observing(KVO)依从于以下key paths:
isCancelled
isConcurrent
isExecuting
isFinished
isReady
dependencies
queuePriority
completionBlock
如果你覆盖start方法,或者对NSOperation对象的其它自定义运行(覆盖main除外),你必须确保自定义对象对这些key paths保留KVO依从。覆盖start方法时,需要关注isExecuting和isFinished两个key paths。
如果你希望实现依赖于其它东西(非operation对象),你可以覆盖isReady方法,并强制返回NO,直到你等待的依赖得到满足。如果你需要保留默认的依赖管理系统,确保你调用了[super isReady]。当你的operation对象的准备就绪状态发生改变时,生成一个isReady的key path的KVO通知。
除非你覆盖了 addDependency: 或 removeDependency: 方法,否则你不需要关注dependencies key path
虽然你也可以生成 NSOperation 的其它KVO通知,但通常你不需要这样做。如果需要取消一个操作,你可以直接调用现有的cancel方法。类似地,你也很少需要修改queue优先级信息。最后,除非你的operation对象可以动态地改变并发状态,你也不需要提供isConcurrent key path的KVO通知。
自定义一个Operation对象的执行行为
对Operation对象的配置发生在创建对象之后,将其添加到queue之前。
配置operation之间的依赖关系
依赖关系可以顺序地执行相关的operation对象,依赖于其它操作,则必须等到该操作完成之后自己才能开始。你可以创建一对一的依赖关系,也可以创建多个对象之间的依赖图。
使用 NSOperation 的 addDependency: 方法在两个operation对象之间建立依赖关系。表示当前operation对象将依赖于参数指定的目标operation对象。依赖关系不局限于相同queue中的operations对象,Operation对象会管理自己的依赖,因此完全可以在不同的queue之间的Operation对象创建依赖关系。
唯一的限制是不能创建环形依赖,这是程序员的错误,所有受影响的operations都无法运行!
当一个operation对象依赖的所有其它对象都已经执行完成,该operation就变成准备执行状态(如果你自定义了isReady方法,则由你的方法确定是否准备好运行)。如果operation已经在一个queue中,queue就可以在任何时候执行这个operation。如果你需要手动执行该operation,就自己调用operation的start方法。
配置依赖必须在运行operation和添加operation到queue之前进行,之后添加的依赖关系可能不起作用。
依赖要求每个operation对象在状态发生变化时必须发出适当的KVO通知。如果你自定义了operation对象的行为,就必须在自定义代码中生成适当的KVO通知,以确保依赖能够正确地执行。
修改Operation的执行优先级
对于添加到queue的Operations,执行顺序首先由已入队列的operations是否准备好,然后再根据所有operations的相对优先级确定。是否准备好由对象的依赖关系确定,优先级等级则是operation对象本身的一个属性。默认所有operation都拥有“普通”优先级,不过你可以通过 setQueuePriority: 方法来提升或降低operation对象的优先级。
优先级只能应用于相同queue中的operations。如果应用有多个operation queue,每个queue的优先级等级是互相独立的。因此不同queue中的低优先级操作仍然可能比高优先级操作更早执行。
优先级不能替代依赖关系,优先级只是queue对已经准备好的operations确定执行顺序。先满足依赖关系,然后再根据优先级从所有准备好的操作中选择优先级最高的那个执行。
修改底层线程的优先级
Mac OS X 10.6之后,我们可以配置operation底层线程的执行优先级,线程直接由内核管理,通常优先级高的线程会给予更多的执行机会。对于operation对象,你指定线程优先级为0.0到1.0之间的某个数值,0.0表示最低优先级,1.0表示最高优先级。默认线程优先级为0.5
要设置operation的线程优先级,你必须在将operation添加到queue之前,调用 setThreadPriority: 方法进行设置。当queue执行该operation时,默认的start方法会使用你指定的值来修改当前线程的优先级。不过新的线程优先级只在operation的main方法范围内有效。其它所有代码仍然(包括completion block)运行在默认线程优先级。
如果你创建了并发operation,并覆盖了start方法,你必须自己配置线程优先级。
设置一个completion block
在Mac OS X 10.6之后,operation可以在主任务完成之后执行一个completion block。你可以使用这个completion block来执行任何不属于主任务的工作。例如你可以使用这个block来通知相关的client,操作已经执行完成。而并发operation对象则可以使用这个block来产生最终的KVO通知。
调用 NSOperation 的 setCompletionBlock: 方法来设置一个completion block,你传递的block应该没有参数和返回值。
实现Operation对象的技巧
Operation对象的内存管理
operation对象需要良好的内存管理策略
创建你自己的Autorelease Pool
operation是Objective-C对象,你在实现任务的代码中应该创建一个autorelease pool,这样可以保护那些autorelease对象得到尽快地释放。虽然你的自定义代码执行时可能已经有了一个pool,但你不能依赖于这个行为,总是应该自己创建一个。
拥有自己的autorelease pool还能更加灵活地管理operation的内存。如果operation创建大量的临时对象,则可以考虑创建额外的pool,来清理不再使用的临时对象。在iOS*****别需要注意,应迟早地清理不再使用的临时对象,避免内存警告。
- - (void)main {
- @try {
- NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
- // Do the main work of the operation here.
- [pool release];
- }
- @catch(...) {
- // Do not rethrow exceptions.
- }
- }
避免Per-Thread存储
虽然多数operation都在线程中执行,但对于非并发operation,通常由operation queue提供线程,这时候queue拥有该线程,而你的应用不应该去动这个线程。特别是不要关联任何数据到不是你创建和拥有的线程。这些线程由queue管理,根据系统和应用的需求创建或销毁。因此使用Per-Thread storage在operations之间传递数据是不可靠的,而且很有可能会失败。
对于operation对象,你完全没有理由使用Per-Thread Storage,应该在创建对象的时候就给它需要的所有数据。所有输入和输出数据都应该存储在operation对象中,最后再整合到你的应用,或者最终释放掉。
根据需要保留Operation对象的引用
由于operation对象异步执行,你不能创建完以后就完全不管。它们也是对象,需要你来分配和释放它们管理的任何资源,特别是如果你需要在operation对象完成后获取其中的数据。
由于queue总是尽最大可能快速地调度和执行operation,在你添加operation到queue时,可能立即就开始运行,当你稍后向queue请求operation对象的状态时,有可能queue已经执行完了相应的operation并从queue中删除了这个对象。因此你总是应该自己拥有operation对象的引用。
处理错误和异常
operation本质上是应用中独立的实体,因此需要自己负责处理所有的错误和异常。NSOperation默认的start方法并没有捕获异常。所以你自己的代码总是应该捕获并抑制异常。你还应该检查错误代码并适当地通知应用。如果你覆盖了start方法,你也必须捕获所有异常,阻止它离开底层线程的范围。
你需要准备好处理以下错误或异常:
检查并处理UNIX errno风格的错误代码
检查方法或函数显式返回的错误代码
捕获你的代码或系统frameworks抛出的异常
捕获NSOperation类自己抛出的异常,在以下情况NSOperation会抛出异常:
operation没有准备好,但是调用了start方法
operation正在执行或已经完成(可能被取消),再次调用了start方法。
当你添加completion block到正在执行或已经完成的operation
当你试图获取已经取消 NSInvocationOperation 对象的结果
为Operation对象确定一个适当的范围
和任何对象一样,NSOperation对象也会消耗内存,执行时也会带来开销。因此如果operation对象只做很少的工作,但是却创建成千上万个小的operation对象,你就会发现更多的时间花在了调度operations而不是执行它们。
要高效地使用Operations,关键是在Operation执行的工作量和保持计算机繁忙之间,找到最佳的平衡。确保每个Operation都有一定的工作量可以执行。例如100个operations执行100次相同任务,可以考虑换成10个operations,每个执行10次。
你同样要避免向一个queue中添加过多的operations,或者持续快速地向queue中添加operation,超过queue所能处理的能力。这里可以考虑分批创建operations对象,在一批对象执行完之后,使用completion block告诉应用创建下一批operations对象。
执行Operations
应用需要执行Operations来处理相关的工作,你有几种方法来执行Operations对象。
添加Operations到Operation Queue
执行Operations最简单的方法是添加到operation queue,后者是 NSOperationQueue 对象。应用负责创建和维护自己使用的所有 NSOperationQueue 对象。
- NSOperationQueue* aQueue = [[NSOperationQueue alloc] init];
调用 addOperation: 方法添加一个operation到queue,Mac OS X 10.6之后可以使用 addOperations:waitUntilFinished: 方法一次添加一组operations,或者也可以直接使用 addOperationWithBlock: 方法添加 block 对象到queue。
- [aQueue addOperation:anOp]; // Add a single operation
- [aQueue addOperations:anArrayOfOps waitUntilFinished:NO]; // Add multiple operations
- [aQueue addOperationWithBlock:^{
- /* Do something. */
- }];
Operations添加到queue后,通常短时间内就会得到运行。但是如果存在依赖,或者Operations挂起等原因,也可能需要等待。
注意Operations添加到queue之后,绝对不要再修改Operations对象。因为Operations对象可能会在任何时候运行,因此改变依赖或数据会产生不利的影响。你只能通过 NSOperation 的方法来查看操作的状态,是否正在运行、等待运行、已经完成等。
虽然 NSOperationQueue 类设计用于并发执行Operations,你也可以强制单个queue一次只能执行一个Operation。setMaxConcurrentOperationCount: 方法可以配置operation queue的最大并发操作数量。设为1就表示queue每次只能执行一个操作。不过operation执行的顺序仍然依赖于其它因素,像操作是否准备好和优先级等。因此串行化的operation queue并不等同于Grand Central Dispatch中的串行dispatch queue。
手动执行Operations
手动执行Operation,要求Operation已经准备好,isReady返回YES,此时你才能调用start方法来执行它。isReady方法与Operations依赖是结合在一起的。
调用start而不是main来手动执行Operation,因为start在执行你的自定义代码之前,会首先执行一些安全检查。而且start还会产生KVO通知,以正确地支持Operations的依赖机制。start还能处理Operations已经被取消的情况,此时会抛出一个异常。
手动执行Operation对象之前,还需要调用 isConcurrent 方法,如果返回NO,你的代码可以决定在当前线程同步执行这个Operation,或者创建一个独立的线程以异步执行。
下面方法演示了手动执行Operation,如果这个方法返回NO,表示不能执行,你需要设置一个定时器,稍后再次调用本方法,直到这个方法返回YES,表示已经执行Operation。
- - (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;
- }
取消Operations
一旦添加到operation queue,queue就拥有了这个对象并且不能被删除,唯一能做的事情是取消。你可以调用Operation对象的cancel方法取消单个操作,也可以调用operation queue的 cancelAllOperations 方法取消当前queue中的所有操作。
只有你确定不再需要Operations对象时,才应该取消它。发出取消命令会将Operations对象设置为"Canceled"状态,会阻止它被执行。由于取消也被认为是完成,依赖于它的其它Operations对象会收到适当的KVO通知,并清除依赖状态,然后得到执行。
因此常见的做法是当发生重大事件时,一次性取消queue中的所有操作,例如应用退出或用户请求取消操作。
等待Operations完成
为了最佳的性能,你应该尽量设计你的应用尽可能地异步操作,让应用在操作正在执行时可以去处理其它事情。
如果创建operation的代码需要处理operation完成后的结果,可以使用 NSOperation 的 waitUntilFinished 方法等待operation完成。通常我们应该避免编写这样的代码,阻塞当前线程可能是一种简便的解决方案,但是它引入了更多的串行代码,限制了整个应用的并发性,同时也降低了用户体验。
绝对不要在应用主线程中等待一个Operation,只能在第二或次要线程中等待。阻止主线程将导致应用无法响应用户事件,应用也将表现为无响应。
除了等待单个Operation完成,你也可以同时等待一个queue中的所有操作,使用 NSOperationQueue 的 waitUntilAllOperationsAreFinished 方法。注意在等待一个queue时,应用的其它线程仍然可以往queue中添加Operation,因此可能加长你线程的等待时间。
挂起和继续Queue
如果你想临时挂起Operations的执行,可以使用 setSuspended: 方法暂停相应的queue。不过挂起一个queue不会导致正在执行的Operation在任务中途暂停,只是简单地阻止调度新Operation执行。你可以在响应用户请求时,挂起一个queue,来暂停等待中的任务。稍后根据用户的请求,可以再次调用 setSuspended: 方法继续Queue中操作的执行。