深入学习多线程之Operation Queue

系统地介绍一下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
2
3
4
5
6
7

NSInvocationOperation* theOp = [[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(myTaskMethod:) object:data];

- (void)myTaskMethod:(id)data {
// 需要执行的任务
}

NSBlockOperation利用block来创建。

1
2
3
4

NSBlockOperation* theOp = [NSBlockOperation blockOperationWithBlock: ^{
// Do some work.
}];

特别注意:NSInvocationOperation可以添加多个block,这些block之间是并行的,但只有当这些block都结束后,操作才会认为自身结束并触发isFinished

1
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
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@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 {
// 开始之前检查状态
if ([self isCancelled])
{
// 手动触发KVO
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}


[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)main {
@try {

for (int i = 0; i < 10000; i ++) {
if([self isCancelled]) {
break;
}
//一些任务
}

[self completeOperation];
}
@catch(...) {
// 异常处理.
}
}

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

executing = NO;
finished = YES;

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

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
8
NSOperationQueue* 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: 方法。该方法不会暂停正在执行的操作,而是会阻止新的操作被执行。

文章目录
  1. 1. Operation Queue
    1. 1.1. 简介
    2. 1.2. 操作的创建
    3. 1.3. 自定义operation对象
      1. 1.3.1. KVO事件
    4. 1.4. operation对象依赖设定
    5. 1.5. 改变operation的优先级
    6. 1.6. 更改operation线程优先级
    7. 1.7. 设置Completion Block
    8. 1.8. operation对象的内存管理
    9. 1.9. 操作的执行
      1. 1.9.1. 添加操作到队列
      2. 1.9.2. 手动执行操作
    10. 1.10. 取消操作
    11. 1.11. 操作串行
    12. 1.12. 暂停与恢复操作
,