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.
}
}
PS:注意你的代码还需要完成所有相关的资源清理工作
为并发执行配置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;
}
//必须重载
//为相应的key path产生适当的KVO通知
//[self willChangeValueForKey:@"isFinished"];
//finished = YES;
//[self didChangeValueForKey:@"isFinished"];
- (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
{
//为相应的key path产生适当的KVO通知
[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对象运行。
如果你覆盖start方法,或者对NSOperation对象的其它自定义运行(覆盖main除外),你必须确保自定义对象对这些key paths保留KVO依从。覆盖start方法时,需要关注isExecuting和isFinished两个key paths。
- (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存储
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挂起等原因,也可能需要等待。
- (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