之前学习GCD的时候,在很多文章中看到过这段段代码:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
}
结果只会输出1,并造成主线程死锁。这些文章对死锁的原因也做了解释,且只要把dispatch_sync改为dispatch_async,就可以输出顺序为1->3->2的结果。
当时以为自己学会了,但是等到用的时候还是一头雾水,之前理解的东西就只是些皮毛。趁着这次学习SDWebImage库,重温了这段代码,才明白自己当时的问题所在。
上述这段代码用来表明死锁的方式没有错,但是对我造成的误区是,笔者以为导致死锁的代码是NSLog(@"2")和NSLog(@"3")。dispatch_sync会阻塞主线程,必须等待NSLog(@"2")执行完后NSLog(@"3")代码才可以执行("3"等待"2")。但是NSLog(@"3")先于NSLog(@"2")被插入到主线程队列中去,NSLog(@"2")必须等到NSLog(@"3")执行完才可以执行("2"等待"3"),这种相互等待的过程导致了主线程死锁。
这种理解方式一直让我深信不疑,但是一次偶然的机会,看到了下面这段代码后,才明白原来的理解方式是错误的。(笔者认为,下面这段代码更能够表现死锁问题。)
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
}
这段代码没有NSLog(@"1"),也没有NSLog(@"3"),但是依然会造成死锁,顿时我就疑惑了,为什么没有NSLog(@"3")还会有问题呢?
在解惑之前,先来看dispatch_sync方法的描述。苹果官方对它的描述是:
Submits a block object for execution on a dispatch queue and waits until that block completes.
翻译过来就是:
提交一个块对象以在分派队列上执行,并等待该块完成。
dispatch_sync方法有两个参数:queue和block。queue表示用于执行block的队列。block是一个代码块,包含了要队列中执行的代码。
队列有三种:
主队列。主队列运行在主线程中,是一个串行队列。
串行队列。遵循先进先出(FIFO)的原则,每次只能执行一个操作。
并发队列。遵循先进先出(FIFO)的原则,每次可以执行多个操作。
队列和执行方式的关系如下表所示:
同步 | 异步 | |
---|---|---|
串行队列(主队列) | 在主线程中执行 | 在主线程中执行 |
串行队列(非主队列) | 在当前线程中执行 | 在新建线程中执行 |
并发队列 | 在当前线程中执行 | 在新建线程中执行 |
- (void)viewDidLoad {dispatch_sync是同步执行方法,会等待第一个参数queue执行完第二个参数block中的代码后,才可以执行dispatch_sync后面的代码。现在,回头再来看上面的代码。
[super viewDidLoad];
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
}
dispatch_sync方法定义在viewDidLoad方法内,viewDidLoad在主线程中执行,所以dispatch_sync方法也在主线程中执行。
dispatch_sync方法的第一个参数传入的时候主队列,说明第二个参数传入的block中的代码也在主线程中运行。
dispatch_sync方法等待queue中的block执行,queue是主队列。但是,由于主队列是串行队列,dispatch_sync比block先加入到主队列中,所以block要等待dispatch_sync执行完才可以执行,这样相互等待的方式最终导致了死锁。
至此,我心中的疑云被解开了,dispatch_sync和block的相互等待才是导致主线程死锁的“真凶”!
解决方法有很多,常见的有以下两种:
将dispatch_sync替换成dispatch_async,即将同步执行改为异步执行。dispatch_async方法会立即返回,允许后面的代码执行,且dispatch_async的block会插入到主队列的末尾,等到后面的代码执行完毕后,再执行block。
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"2");
// 打印当前队列的标签
NSLog(@"%s", dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));
});
// print 2
// print com.apple.main-thread 主队列标签
创建一个串行队列(非主队列)或并发队列。创建了一个新的队列,不会影响主队列执行,因为是同步执行方法,所以主队列会等到新队列执行完block后才继续执行。
// 串行队列
dispatch_queue_t queue = dispatch_queue_create("queue", nil);
// 并发队列
// dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, nil), ^{
NSLog(@"2");
// 打印当前队列的标签
NSLog(@"%s", dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));
});
// print 2
// print queue 串行队列标签
以上两种解决方法,代码依然运行在主线程,前一种方法改为了异步执行,后一种方法创建新的队列。
#ifndef dispatch_queue_async_safe
#define dispatch_queue_async_safe(queue, block)
if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(queue)) {
block();
} else {
dispatch_async(queue, block);
}
#endif
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block) dispatch_queue_async_safe(dispatch_get_main_queue(), block)
#endif
dispatch_queue_async_safe方法接收两个参数queue和block,该方法判断传入queue的标签是否等于当前队列的标签,返回YES,则在queue(当前队列)中执行block,否则将block添加到queue中异步执行。
dispatch_main_async_safe方法接收一个参数block,内部调用dispatch_queue_async_safe方法,传入queue的值为主队列dispatch_get_main_queue()和block,dispatch_queue_async_safe方法判断当前队列是否为主队列,返回YES则直接执行block的代码,否则将block添加到主队列中异步执行。
这个宏可以保证,block中的代码一定在主队列中执行。
其实理解dispatch_sync的关键就是搞清楚执行代码所在的队列是哪个,只要执行dispatch_sync的队列跟dispatch_sync第一个参数传入的队列相同,就会产生死锁。
还有,看苹果的官方文档是最正确的方式,虽然英文看上去累一些,但是能够准确表达每一个类,每一行代码的意义。中文翻译过来的语言都会带上作者自己的一些理解。官方文档对于GCD的描述如下:
GCD provides and manages FIFO queues to which your application can submit tasks in the form of block objects. Work submitted to dispatch queues are executed on a pool of threads fully managed by the system. No guarantee is made as to the thread on which a task executes.
请认真研读下面这句话,会对理解调度队列和线程之间的关系有帮助。
Work submitted to dispatch queues are executed on a pool of threads fully managed by the system.
GCD官方文档地址:https://developer.apple.com/documentation/dispatch#//apple_ref/doc/uid/TP40008079