【iOS】OC底层系列十三 - 多线程-GCD

iOS中的常见多线程方案:

一、GCD的常用函数

GCD中有2个用来执行任务的函数:

  1. 用同步的方式执行任务(当前线程)

    1
    2
    3
    // queue:队列
    // block:任务
    dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
  2. 用异步的方式执行任务(子线程)

    1
    dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

GCD源码:https://github.com/apple/swift-corelibs-libdispatch

GCD的队列可以分为2大类型:

  1. 并发队列(Concurrent Dispatch Queue

    • 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
    • 并发功能只有在异步(dispatch_async)函数下才有效
  2. 串行队列(Serial Dispatch Queue

    • 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)

有4个术语比较容易混淆:同步、异步、并发、串行。

  • 同步和异步主要影响:能不能开启新的线程

    • 同步:在当前线程中执行任务,不具备开启新线程的能力
    • 异步:在新的线程中执行任务,具备开启新线程的能力
  • 并发和串行主要影响:任务的执行方式

    • 并发:多个任务并发(同时)执行
    • 串行:一个任务执行完毕后,再执行下一个任务

dispatch_asyncdispatch_sync用来控制是否要开启新的线程。队列(dispatch_queue)的类型决定了任务的执行方式是并发还是串行,主队列也是一个串行队列。

二、死锁(经典面试题)

使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)

2.1. 示例代码一

1
2
3
4
5
6
7
8
9
10
- (void)viewDidLoad {
[super viewDidLoad];

NSLog(@"1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{ // 线程1
NSLog(@"2");
});
NSLog(@"3");
}

会产生死锁。输出顺序:1,然后到线程1会卡死,不会输出2和3。

由于队列是先入先出的,viewDidLoad的执行优先级比dispatch_sync高,所以需要等待viewDidLoad方法执行结束后才能执行同步线程中的任务。但由于是同步串行任务,因此会等待主线程的任务执行结束后(执行viewDidLoad)再往后执行。此时,viewDidLoaddispatch_sync会形成相互等待的状态(也就是死锁),最终导致程序崩溃。

2.2. 示例代码二

在示例代码一的基础上把dispatch_sync修改为dispatch_async

1
2
3
4
5
6
7
8
9
10
- (void)viewDidLoad {
[super viewDidLoad];

NSLog(@"1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(dispatch_get_main_queue(), ^{ // 线程1
NSLog(@"2");
});
NSLog(@"3");
}

不会产生死锁。输出顺序:1 - 3 - 2

dispatch_async不要求立马在当前线程同步执行任务。线程是添加到主队列中执行的,虽然是异步任务,但不会开启新的线程。

2.3. 示例代码三

在示例代码二的基础上把队列修改为创建的串行队列,并且在异步线程中向队列中添加同步线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)viewDidLoad {
[super viewDidLoad];

NSLog(@"1");
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 线程1
NSLog(@"2");
dispatch_sync(queue, ^{ // 线程2
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}

会产生死锁。输出顺序:1 - 5 - 2,然后到线程2会卡死,不会输出3和4。

和示例代码一的原因一样,线程1和线程2都添加到同一个串行队列中,线程1执行结束的前提条件是线程2的任务已经执行结束,线程2执行结束的前提条件是线程1的任务已经执行结束,所以会处在相互等待的状态(死锁)。

2.4. 示例代码四

在示例代码三的基础上把线程2添加到创建的并发队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)viewDidLoad {
[super viewDidLoad];

NSLog(@"1");
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("myqueue2", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ // 线程1
NSLog(@"2");
dispatch_sync(queue2, ^{ // 线程2
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}

不会产生死锁。输出顺序:1 - 5 - 2 - 3 - 4

因为创建了2个不同的队列,当前每一个队列都只有一个任务,并且都是独立执行任务的,所以不会产生死锁。

2.5. 示例代码五

在示例代码四的基础上把线程2添加到创建的串行队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)viewDidLoad {
[super viewDidLoad];

NSLog(@"1");
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("myqueue2", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 线程1
NSLog(@"2");
dispatch_sync(queue2, ^{ // 线程2
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}

不会产生死锁。输出顺序:1 - 5 - 2 - 3 - 4

道理同示例代码四,两个不同的队列单独执行任务。

2.6. 示例代码六

在示例代码四的基础上把线程1添加也到并发队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)viewDidLoad {
[super viewDidLoad];

NSLog(@"1");
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ // 线程1
NSLog(@"2");
dispatch_sync(queue, ^{ // 线程2
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}

不会产生死锁。输出顺序:1 - 5 - 2 - 3 - 4

因为是并发队列,所以线程1和线程2会独立执行。

三、队列组

思考:如何用GCD实现以下功能。
异步并发执行任务1、任务2。等任务1、任务2都执行完毕后,再回到主线程执行任务3。

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
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建并发队列
dispatch_queue_t queue = dispatch_queue_create("my_queue", DISPATCH_QUEUE_CONCURRENT);

// 添加异步任务
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务1-%@", [NSThread currentThread]);
}
});

dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务2-%@", [NSThread currentThread]);
}
});

// 等前面的任务执行完毕后,会自动执行这个任务
dispatch_group_notify(group, queue, ^{
// 回到主线程执行任务
dispatch_async(dispatch_get_main_queue(), ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务3-%@", [NSThread currentThread]);
}
});
});

/* 上面的代码和下面的代码是等价的
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务3-%@", [NSThread currentThread]);
}
});
*/

// 也可以在任务1和任务2执行完毕后,在同一个队列多次执行dispatch_group_notify继续异步执行任务4和任务5
/*
dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务4-%@", [NSThread currentThread]);
}
});

dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务5-%@", [NSThread currentThread]);
}
});
*/

面试题1:下面代码执行结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)viewDidLoad {
[super viewDidLoad];

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
});
}

- (void)test {
NSLog(@"2");
}

输出:1 - 2,不会输出3。

如果把afterDelay取消,使用performSelector:withObject:就会输出1 - 2 - 3

如果把任务放到主队列中,会输出1 - 3 - 2

performSelector:withObject:afterDelay是RunLoop的一个扩展方法,它的本质是往RunLoop中添加定时器,但子线程默认是没有启动RunLoop的,所以上面的代码不会执行test方法。要想解决这个问题,只需要启动RunLoop就可以。

1
2
3
4
5
6
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});

面试题2:下面代码执行结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)viewDidLoad {
[super viewDidLoad];

NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");
}];
[thread start];

[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}

- (void)test {
NSLog(@"2");
}

输出结果是:1,并且程序会崩溃。

由于blocktest是在同一个线程上执行,并且会优先执行block中的任务,但是执行完block中的任务后线程就死掉了,所以再执行test方法时会报错。

正确做法(线程保活):

1
2
3
4
5
6
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// 不能调用[[NSRunLoop currentRunLoop] run],否则线程就没办法停下
}];

GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍(Foundation等一些框架是不开源的),虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值。

源码地址:http://www.gnustep.org/resources/downloads.php