发布于 

iOS 多线程 - NSOperation

iOS 日常开发过程中,涉及到多线程处理的需求,绝大多数可以通过 GCD 来完成。但如果想要给 task 添加依赖、取消、暂停、恢复的需求,GCD 的实现就会变得很复杂。这时就引申除了 NSOperation,NSOperation 是基于 GCD 的封装,提供面向对象的形式。我们可以借助 NSOperation 将每个 task 封装为一个个对象再进行操作,这也使得线程处理的代码逻辑更为清晰易懂。

简而言之,NSOperation 相对的优势有:

  • 可以添加操作间的依赖关系
  • 可以设定操作执行的优先级
  • 可以很方便的取消一个操作的执行
  • 可以通过 KVO 观察操作执行的状态:
    • isExecuting
    • isFinished
    • isCancelled

NSOperation 用法

NSOperation 需要配合 NSOperationQueue 来实现多线程异步执行。默认情况下,NSOperation 单独使用时,系统在当前线程中同步执行操作。NSOperation 是个抽象类,不能用来封装操作。我们可以通过它的子类来封装操作。封装方式有三种:

  • 使用子类 NSInvocationOperation
  • 使用子类 NSBlockOperation
  • 使用自定义继承自 NSOperation 的子类

先在不使用 NSOperationQueue 的情况下单独使用 NSOperation(系统同步执行),看下三种创建方式。

NSInvocationOperation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)useInvocationOperation {
// 1.创建 NSInvocationOperation 对象
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
// 2.调用 start 方法开始执行操作
[op start];
}
- (void)task1 {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"thread---%@", [NSThread currentThread]); // 打印当前线程
}
}

// 打印日志
2016-03-10 22:04:09.848818+0800 Demo[985:5832682] thread---<NSThread: 0x600002f54140>{number = 1, name = main}
2016-03-10 22:04:11.849534+0800 Demo[985:5832682] thread---<NSThread: 0x600002f54140>{number = 1, name = main}

看到默认在当前线程中执行操作。因为代码是在主线程中调用的,所以打印结果为主线程。如果在其他线程中执行操作,则打印结果为其他线程。

NSBlockOperation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)useBlockOperation {
// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 2.调用 start 方法开始执行操作
[op start];
}

// 打印日志
2016-03-10 22:32:58.166336+0800 BuildDemo[1309:5851093] thread---<NSThread: 0x6000022e0dc0>{number = 1, name = main}
2016-03-10 22:33:00.167871+0800 BuildDemo[1309:5851093] thread---<NSThread: 0x6000022e0dc0>{number = 1, name = main}

同样默认在当前线程执行操作。但 blockOperationWithBlock: 如果通过 addExecutionBlock: 添加多个操作时,操作有可能会在其他线程(非当前线程)中执行。

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
- (void)useBlockOperationAddExecutionBlock {
// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 2.添加额外的操作
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"5---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"6---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"7---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"8---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 3.调用 start 方法开始执行操作
[op start];
}

// 打印日志ß
2016-03-10 22:41:06.732530+0800 BuildDemo[1355:5855507] 4---<NSThread: 0x600000203500>{number = 7, name = (null)}
2016-03-10 22:41:06.732562+0800 BuildDemo[1355:5855387] 1---<NSThread: 0x60000026c500>{number = 1, name = main}
2016-03-10 22:41:06.732563+0800 BuildDemo[1355:5855506] 3---<NSThread: 0x600000227e80>{number = 6, name = (null)}
2016-03-10 22:41:06.732600+0800 BuildDemo[1355:5855510] 2---<NSThread: 0x600000220a40>{number = 3, name = (null)}
2016-03-10 22:41:08.733836+0800 BuildDemo[1355:5855507] 4---<NSThread: 0x600000203500>{number = 7, name = (null)}
2016-03-10 22:41:08.733836+0800 BuildDemo[1355:5855387] 1---<NSThread: 0x60000026c500>{number = 1, name = main}
2016-03-10 22:41:08.733886+0800 BuildDemo[1355:5855510] 2---<NSThread: 0x600000220a40>{number = 3, name = (null)}
2016-03-10 22:41:08.733890+0800 BuildDemo[1355:5855506] 3---<NSThread: 0x600000227e80>{number = 6, name = (null)}
2016-03-10 22:41:10.735331+0800 BuildDemo[1355:5855510] 7---<NSThread: 0x600000220a40>{number = 3, name = (null)}
2016-03-10 22:41:10.735331+0800 BuildDemo[1355:5855506] 8---<NSThread: 0x600000227e80>{number = 6, name = (null)}
2016-03-10 22:41:10.735333+0800 BuildDemo[1355:5855387] 5---<NSThread: 0x60000026c500>{number = 1, name = main}
2016-03-10 22:41:10.735332+0800 BuildDemo[1355:5855507] 6---<NSThread: 0x600000203500>{number = 7, name = (null)}
2016-03-10 22:41:12.735989+0800 BuildDemo[1355:5855506] 8---<NSThread: 0x600000227e80>{number = 6, name = (null)}
2016-03-10 22:41:12.735989+0800 BuildDemo[1355:5855510] 7---<NSThread: 0x600000220a40>{number = 3, name = (null)}
2016-03-10 22:41:12.736083+0800 BuildDemo[1355:5855387] 5---<NSThread: 0x60000026c500>{number = 1, name = main}
2016-03-10 22:41:12.736083+0800 BuildDemo[1355:5855507] 6---<NSThread: 0x600000203500>{number = 7, name = (null)}

根据打印结果,我们看到线程是有变化的。NSBlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。

自定义继承自 NSOperation 的子类

如果如上两种无法满足需求,我们可以自己自定义继承自 NSOperation 的子类。通过重写 mainstart 方法来自定义 NSOperation 对象。

先看下重写 main 的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// FFOperation.h 文件
#import <Foundation/Foundation.h>

@interface FFOperation : NSOperation
@end

// FFOperation.m 文件
#import "FFOperation.h"

@implementation FFOperation

- (void)main {
if (!self.isCancelled) {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"1---%@", [NSThread currentThread]);
}
}
}

@end

使用:

1
2
3
4
- (void)useCustomOperation {
FFOperation *op = [[FFOperation alloc] init];
[op start];
}

在没有使用 NSOperationQueue 时,在当前线程执行,不开启新线程。

NSOperationQueue

配置 maxConcurrentOperationCount

NSOperationQueue 有两种队列:

  • 主队列
  • 自定义队列
    • 串行
    • 并发

其中自定义队列是串行或并发,由最大并发数控制。示例代码:

1
2
3
4
5
6
7
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

// 2.设置最大并发操作数
queue.maxConcurrentOperationCount = 1; // 串行队列
queue.maxConcurrentOperationCount = 2; // 并发队列
queue.maxConcurrentOperationCount = 8; // 并发队列

配置依赖

NSOperation 能添加操作之间的依赖关系。NSOperation 提供了 3 中接口供管理和查看依赖:

1
2
3
4
5
6
7
8
// 添加依赖
- (void)addDependency:(NSOperation *)op;

// 移除依赖
- (void)removeDependency:(NSOperation *)op;

// 可用于查看依赖
@property (readonly, copy) NSArray<NSOperation *> *dependencies;

以操作 op2 依赖于操作 op1 为例:

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
- (void)addDependency {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.创建操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 3.添加依赖
// 让 op2 依赖于 op1,则先执行 op1,再执行 op2
[op2 addDependency:op1];
// 4.添加操作到队列中
[queue addOperation:op1];
[queue addOperation:op2];
}

// 打印日志
2016-03-11 10:57:13.394399+0800 Demo[5104:6020710] 1---<NSThread: 0x600001df2a00>{number = 7, name = (null)}
2016-03-11 10:57:15.397272+0800 Demo[5104:6020710] 1---<NSThread: 0x600001df2a00>{number = 7, name = (null)}
2016-03-11 10:57:17.401159+0800 Demo[5104:6020710] 2---<NSThread: 0x600001df2a00>{number = 7, name = (null)}
2016-03-11 10:57:19.406361+0800 Demo[5104:6020710] 2---<NSThread: 0x600001df2a00>{number = 7, name = (null)}

根据打印日志,可以看到无论运行几次,结果都是 op1 先执行,op2 后执行。

配置优先级

NSOperation 提供了 queuePriority(优先级) 属性,可以用于同一队列中的操作。默认新创建的操作对象的优先级都为 NSOperationQueuePriorityNormal。我们可以通过 setQueuePriority: 方法来改变当前操作在同一队列中的执行优先级。优先级有:

1
2
3
4
5
6
7
8
// 优先级的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};

对于添加到队列中的操作,先根据依赖关系进入就绪状态,再根据相对的优先级开始执行操作。

什么样的操作才能进入就绪状态?

  • 当一个操作的所有依赖都已经完成时,该操作对象通常会进入就绪状态,等待执行。

queuePriority 属性的作用对象:

  • queuePriority 属性可以决定进入就绪状态的操作之间的开始执行顺序。优先级不能取代依赖。
  • 若一个队列中同时包含高优先级操作和低优先级操作,且两个操作都进入了就绪状态,则队列先执行高优先级操作。
  • 若一个队列同时包含了就绪和未就绪的操作,且未就绪的操作的优先级比就绪操作的优先级高。会先执行已经就绪的相对低优先级的操作。优先级不能取代依赖关系。如果要控制操作间的开始执行顺序,需要通过依赖关系处理。

线程间的通信

通常我们会把耗时操作(图片下载、文件上传等)放到其他线程操作,当这些耗时操作完成时,需要到主线程中对应作出回调处理。这是比较常遇到的一个线程间通信的场景,示例代码:

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
- (void)communication {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
// 2.添加操作
[queue addOperationWithBlock:^{
// 异步进行耗时操作
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
// 回到主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 进行一些 UI 刷新等操作
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
}];
}

// 打印日志
2016-03-11 12:03:55.281088+0800 Demo[5557:6061169] 1---<NSThread: 0x6000022768c0>{number = 6, name = (null)}
2016-03-11 12:03:57.281512+0800 Demo[5557:6061169] 1---<NSThread: 0x6000022768c0>{number = 6, name = (null)}
2016-03-11 12:03:59.282192+0800 Demo[5557:6061041] 2---<NSThread: 0x60000223cbc0>{number = 1, name = main}
2016-03-11 12:04:01.283569+0800 Demo[5557:6061041] 2---<NSThread: 0x60000223cbc0>{number = 1, name = main}

通过 log 可以看到线程的切换,达到了线程间通信的目的。

线程同步和线程安全

线程安全的概念:当进程中有多个线程同时对某个操作进行操作,如果多线程运行结果和单线程运行结果一致,就表示是线程安全的。一般多线程如果只进行读操作,是线程安全的;若多线程同时有读写操作,需要考虑线程同步,保证线程的安全。

线程同步的概念:自己理解为,现在同时有线程 A 和 B,A 执行到一定程度后需要依赖 B 的处理结果,这是 A 就需要停下来等待 B 给到处理结果后,再进行下面的操作。

举一个需要线程安全的例子:现有两个不同售票窗口,共有 10 张票,两个同时售票,卖完为止。

这里使用 NSLock 来保证线程同步,实例代码如下:

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
- (void)initTicketStatusSave {
// 打印当前线程
NSLog(@"currentThread---%@",[NSThread currentThread]);

self.ticketCount = 10;
// 初始化 NSLock 对象
self.lock = [[NSLock alloc] init];

// 1.创建 queue1, queue1 代表 A 售票窗口
NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
queue1.maxConcurrentOperationCount = 1;

// 2.创建 queue2, queue2 代表 B 票票窗口
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
queue2.maxConcurrentOperationCount = 1;

// 3.创建卖票操作 op1
__weak typeof(self) weakSelf = self;
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[weakSelf saleTicketSafe];
}];

// 4.创建卖票操作 op2
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[weakSelf saleTicketSafe];
}];

// 5.添加操作,开始卖票
[queue1 addOperation:op1];
[queue2 addOperation:op2];
}

- (void)saleTicketSafe {
while (1) {
// 加锁
[self.lock lock];
// 如果还有票,继续售卖
if (self.ticketCount > 0) {
self.ticketCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", (long)self.ticketCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
}
// 解锁
[self.lock unlock];

if (self.ticketCount <= 0) {
NSLog(@"票已售完");
break;
}
}
}

// 打印日志
2016-03-11 12:28:47.155040+0800 Demo[5814:6075398] currentThread---<NSThread: 0x600003118c00>{number = 1, name = main}
2016-03-11 12:28:47.156066+0800 Demo[5814:6075448] 剩余票数:9 窗口:<NSThread: 0x600003152600>{number = 7, name = (null)}
2016-03-11 12:28:47.357156+0800 Demo[5814:6075448] 剩余票数:8 窗口:<NSThread: 0x600003152600>{number = 7, name = (null)}
2016-03-11 12:28:47.560797+0800 Demo[5814:6075448] 剩余票数:7 窗口:<NSThread: 0x600003152600>{number = 7, name = (null)}
2016-03-11 12:28:47.773323+0800 Demo[5814:6075448] 剩余票数:6 窗口:<NSThread: 0x600003152600>{number = 7, name = (null)}
2016-03-11 12:28:47.973762+0800 Demo[5814:6075448] 剩余票数:5 窗口:<NSThread: 0x600003152600>{number = 7, name = (null)}
2016-03-11 12:28:48.248293+0800 Demo[5814:6075449] 剩余票数:4 窗口:<NSThread: 0x60000312bb00>{number = 4, name = (null)}
2016-03-11 12:28:48.510233+0800 Demo[5814:6075448] 剩余票数:3 窗口:<NSThread: 0x600003152600>{number = 7, name = (null)}
2016-03-11 12:28:49.111171+0800 Demo[5814:6075449] 剩余票数:2 窗口:<NSThread: 0x60000312bb00>{number = 4, name = (null)}
2016-03-11 12:28:49.507041+0800 Demo[5814:6075449] 剩余票数:1 窗口:<NSThread: 0x60000312bb00>{number = 4, name = (null)}
2016-03-11 12:28:49.848690+0800 Demo[5814:6075449] 剩余票数:0 窗口:<NSThread: 0x60000312bb00>{number = 4, name = (null)}
2016-03-11 12:28:50.053556+0800 Demo[5814:6075449] 票已售完
2016-03-11 12:28:50.053589+0800 Demo[5814:6075448] 票已售完

通过 NSLock 加锁解锁,得到的票数是正确的,这也解决了多线程同步的问题。

总结

汇总下 NSOperation 和 NSOperationQueue 的常用属性和方法。

NSOperation

  1. 取消操作:

    1
    2
    // 取消操作,实质是标记 isCancelled 状态
    - (void)cancel;
  2. 判断操作的状态:

    1
    2
    3
    4
    5
    6
    7
    8
    // 判断操作是否结束
    - (BOOL)isFinished;
    // 判断操作是否已标记为取消
    - (BOOL)isCancelled;
    // 判断操作是否正在运行
    - (BOOL)isExecuting;
    // 判断操作是否已进入就绪状态(就绪状态与依赖关系有关)
    - (BOOL)isReady;
  3. 操作同步

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 阻塞当前线程,知道对应操作结束。用于线程同步。
    - (void)waitUntilFinished;
    // 当操作处理完成,block 回调
    - (void)setCompletionBlock:(void (^)(void))block;
    // 添加依赖(让当前操作依赖 op)
    - (void)addDependency:(NSOperation *)op;
    // 移除依赖(取消当前操作的依赖)
    - (void)removeDependency:(NSOperation *)op;
    // 当前操作所依赖的所有操作对象
    @property (readonly, copy) NSArray<NSOperation *> *dependencies;

NSOperationQueue

  1. 取消、暂停、恢复操作:

    1
    2
    3
    4
    5
    6
    // 取消 queue 的所有操作
    - (void)cancelAllOperations;
    // 判断队列是否在暂停状态
    - (BOOL)isSuspended;
    // 暂停或恢复当前队列中的操作(YES 暂停,NO 恢复)
    - (void)setSuspended:(BOOL)b;
  2. 操作同步

    1
    2
    // 阻塞当前线程,知道队列中所有的操作完成
    - (void)waitUntilAllOperationsAreFinished;
  3. 添加、获取操作:

    1
    2
    3
    4
    5
    6
    7
    8
    // 向队列添加操作对象
    - (void)addOperationWithBlock:(void (^)(void))block;
    // 向队列添加多个操作对象,wait 表示是否阻塞当前线程知道队列中所有操作完成
    - (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait;
    // 获取当前队列中所有的操作对象(一个操作结束后,会从该数组移除)
    - (NSArray *)operations;
    // 当前队列中的操作数
    - (NSUInteger)operationCount;
  4. 获取队列:

    1
    2
    3
    4
    // 获取当前队列
    + (id)currentQueue;
    // 获取主队列
    + (id)mainQueue;

其他

关于取消和恢复:

  • 可以通过 -setSuspended: 暂停或恢复当前 queue 的操作
  • Operation Queue 的取消和暂停,指当当前在执行的 Operation 执行完毕后,不再执行新的 Operation。暂停的操作可以恢复,但如果操作被取消,该操作会被清空,无法再恢复执行。

参考内容:


本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本站由 @JonyFang 创建,使用 Stellar 作为主题,您可以在 GitHub 找到本站源码。