系统地介绍一下iOS多线程中的功能最为强大的Operation Queue
Operation Queue
简介
操作队列(Operation Queue)是iOS实现多线程的一种重要途径。操作队列可以看做是Cocoa对调度队列(dispatch queue)的一个封装。在iOS上开发多线程是非常幸运的,因为系统为我们抽象出了队列这个概念。这使得我们不必与底层的线程打交道,只需提交一个操作(operation)到队列中就可以了,之后创建线程、分配任务的一系列工作都自动完成了。
虽然我们总是用Operation Queue来代表该多线程技术,但由于队列基本都是系统提供的,这部分内容的重点其实是operation。iOS为我们提供了三种操作类:
- NSInvocationOperation
- NSBlockOperation
- NSOperation
NSInvocationOperation适用于已经存在method或多个操作有大量共通处的场合,NSBlockOperation适合使用block,NSOperation是前两者的基类,适合需要对操作进行深入定制的场合。
三种操作类都支持如下特性:
- 支持基于图的依赖指定
- 支持complete时block指定,在队列主任务完成时触发
- 支持基于KVO的执行状态变更通知
- 支持队列取消
操作的创建
NSInvocationOperation通过指定selector来创建,可以避免产生大量自定义的操作
1 |
|
NSBlockOperation利用block来创建。1
2
3
4
NSBlockOperation* theOp = [NSBlockOperation blockOperationWithBlock: ^{
// Do some work.
}];
特别注意
:NSInvocationOperation可以添加多个block,这些block之间是并行的,但只有当这些block都结束后,操作才会认为自身结束并触发isFinished1
2
3
4
5
6[blockOperation addExecutionBlock:^{
//任务一
}];
[blockOperation addExecutionBlock:^{
//任务二
}];
自定义operation对象
若上述两种operation不能满足需求,我们可以自定义NSOperation子类来自定义。
操作对象默认同步执行,即它们在调用它们的线程中执行start函数。加入到操作队列中会自动并行化运行,但如果需要通过start运行,则需要进行自定义:
- start
做好运行环境的准备,如果需要并行,则需要创建线程。注意在其中不能调用super方法。根据操作状态决定是否执行main函数 - main
操作的任务主体。建议在耗时操作中进行过程中添加对isCancelled的判断,以便能及时终止 - isExecuting isFinished
提供状态信息 - isConcurrent
若要并行则需要返回YES
需要注意的是,NSOperation提供的KVO模式,当重写operation时需要手动触发。
1 | @interface MyOperation : NSOperation { |
KVO事件
NSOperation提供了一系列KVO事件用于监听操作的状态变化
- isCancelled 用于监听操作是否被取消
- isConcurrent 用于监听操作是否允许并行
- isExecuting 用于监听操作是否正在进行
- isFinished 用于监听操作是否完成
- isReady 用于监听操作是否可以开始执行了(用于有依赖的情况)
- dependencies 用于监听操作的依赖操作
- queuePriority 用于监听操作所在的队列的优先级发生变化的场合
- completionBlock 用于监听操作的完成回调
operation对象依赖设定
依赖是一种很好的为不同操作对象添加时序的方法。一个依赖于其他操作的操作只有当依赖操作全部结束后才能运行。
可以通过NSOperation的addDependency: 方法使得当前对象依赖于指定操作。依赖不限于同一队列的操作,但绝不能产生环。
当一个操作的依赖全部执行完成后会触发isReady,进而会运行start。
操作的依赖必须在添加进队列前就设置完毕。
依赖的实现是通过每个操作的KVO信号,故自定义的操作一定要覆写信号否则不能正确产生依赖。
改变operation的优先级
添加入队列的操作的执行顺序首先由操作是否准备完毕决定,其次由它们的优先级决定。准备是否完毕由操作的依赖决定,但优先级是操作自身的属性。一个新的操作对象默认是normal优先级,可以通过setQueuePriority:更改优先级。
优先级的设置只对同一队列中的操作有效。在不同的队列中,低优先级的操作可能比另一队列中的高优先级操作先运行。
优先级不能够用于依赖。优先级只决定了处于ready状态的操作的运行顺序。
更改operation线程优先级
我们还可以修改一个操作所在线程的执行优先级。线程的执行逻辑是由内核控制的,但一般来说,高优先级线程运行的可能性高于低优先级线程。可以指定优先级为0.0到1.0,0.0为最低优先级。操作所在的线程默认为0.5
可以通过setThreadPriority:来指定线程优先级。操作在start执行时回设置自己的线程优先级为设定值,并仅在main方法中保持该优先级。在其他方法中,如completion block中会恢复默认线程优先级。若手动创建异步线程覆写了start则需要手动设定线程优先级。
设置Completion Block
可以通过setCompletionBlock:设置结束时的block进行结束事件通知。特别注意
,Completion Block总是重新创建一个线程运行,即使操作运行的队列是主队列。
operation对象的内存管理
多数的操作执行在操作队列提供的线程上。这些线程应当被认为是被队列拥有,操作不应当接触它们。即在任何时候都不应该将数据关联到非自己创造的线程上。这些操作队列创建的线程的进出是由系统决定的,不应该通过单独线程存储来传递数据。
当创建操作对象时,应当在初始化中提供一切任务需要的数据,所有的中间数据都应当被保存在对象中直到不再需要。
由于操作对象是异步的,它们仍是对象,需要代码手动管理引用,尤其是需要在操作结束后获取其中的数据。
由于当操作结束后,操作可能会立刻从队列中移除,所以如果需要访问操作就必须提前保存下来引用。
虽然可以向队列中添加大量的操作,但操作对象的创建与运行存在开销。需要平衡好操作的数量以确保每个操作都有合适量的任务执行。
操作的执行
添加操作到队列
到目前,最简单的执行操作的方式是利用操作队列。应用为了使用操作队列,应当负责创建和保持工作。一个应用可以有很多队列,但在某一时刻可执行的操作是受到限制的。操作队列会根据可用核数和系统负载来控制并行的操作数。因此增加队列数并不能提高可执行的操作数。
每一个操作最多只能添加到一个操作队列中,当操作处于正在执行状态或已完成状态时,不能添加到队列。
我们可以通过调用NSOperationQueue的mainQueue方法来获取到主队列。主队列中的任务都是由主线程进行。1
2
3
4
5
6
7
8NSOperationQueue* aQueue = [[NSOperationQueue alloc] init];
//添加一个操作
[aQueue addOperation:anOp];
//添加多个操作,waitUntilFinished若为YES则会阻塞线程知道全部操作结束
[aQueue addOperations:anArrayOfOps waitUntilFinished:NO];
[aQueue addOperationWithBlock:^{
//block任务
}];
每一个添加方法会添加操作到队列,并通知队列需要执行该操作。在多数情况下,操作会在加入队列的后很短时间内执行,但队列会基于一些原因延迟执行操作:例如依赖的操作没有执行完毕,或队列被暂停或已经运行了最大数目的操作。
虽然NSOperationQueue被设计为并行运行操作,但我们可以通过setMaxConcurrentOperationCount:使其串行化,但串行的执行顺序是受依赖和优先级决定的,不完全依靠添加顺序。
手动执行操作
虽然操作队列是最方便的运行操作的,但仍有不用队列运行操作的需求。若需要手工执行操作,需要对代码做一些修改。
一个操作只有当isReady方法返回yes是才能运行。isReady时由依赖管理系统控制的。
当手动执行操作时,必须使用start而非其他方法,因为start提供了各种安全性检查,生成KVO通知,并防止了出现异常产生。
若应用中指定了并行操作对象,还需用isConcurrent方法来确定是否需要并行化运行某个操作。
以下代码展示了手动执行操作的步骤: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- (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])
{
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
ranIt = YES;
}
return ranIt;
}
取消操作
一旦操作被添加入队列,操作就不能从队列中移除,唯一的移除办法是取消操作。我们可以通过调用operation的cancel函数来取消一个操作的进行,或通过队列的cancelAllOperations方法取消队列中的所有操作自定义操作的main方法中加入isCancelled的判断,NSInvocationOperation和NSBlockOperation则需要分别在method和block中加入isCancelled的判断,否则该方法无法生效。
一旦操作被取消,该操作将不会再被运行,队列将视其为完成。
操作串行
为了高性能,操作应当尽可能并行化。若创建操作的程序块需要处理该操作结束后的结果,则可以用NSOperation的waitUntilFinished使得该操作会被阻塞直到操作结束。一般来说,最好不要进行这种操作,虽然这可以提高代码的串行性,减少并行量。
可以通过NSOperationQueue的waitUntilAllOperationsAreFinished方法等待队列中所有操作结束。在等待结束时,其他的线程仍有可能像队列中提交新的操作,从而延长等待时间。
暂停与恢复操作
若想要暂停操作的执行,可以通过调用队列的setSuspended: 方法。该方法不会暂停正在执行的操作,而是会阻止新的操作被执行。