逛到 stackoverflow 上面一篇提问,引起了我的深思,写了这么久的代码,反过头来去看最最基本的东西,却又觉得十分有趣。不废话了,直接上链接:
https://stackoverflow.com/questions/19180661/sync-dispatch-on-current-queue
引起我兴趣的主要是最下面的回复,我截图下来了,不能连外网的朋友可以看一下:
可以看到,上面有四个示例,其中的两个死锁了,另外两个正常运行。先来普及一下死锁的概念再解释原因。
死锁:在一个串行队列Q中执行任务A,可是此时又要在任务A中同步执行任务B,将任务B也是添加到了串行队列Q中,这样就会发生死锁。
就如同上面两个死锁的情况,外层 block 要等待执行完毕后才算完成,而此时在外层 block 内部又加了一个任务,而这个任务强制要同步执行,强制插入,这样一来就必须先执行内部的任务,可以此时线程队列被外层 block 占用着,必须等待外层 block 走到头才可以走新添加的任务。这样两个任务牵制彼此,都无法完成。
那么我们改一下 Situation 3 中的代码:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self goDoSomethingLongAndInvolved];
dispatch_sync(queue, ^{
NSLog(@"Situation 3");
});
});
Situation 4 同理。
Situation 1 和 Situation 4 的区别在于外层 block 是异步调用还是同步调用,但异步和同步是相对于同一线程下同级的任务而言的,所以说这里的任务只存在外层的 block 一个,所以说异步同步就没有区别了。
写了几个个小 Demo,大家可以分析一下。
第一个 demo,初步理解:
两个 VC,YellowVC push 到 GreenVC,在 GreenVC 中添加一个 pop 按钮。按钮的点击方法实现如下:
- (IBAction)btnClick2:(UIButton *)sender {
self.myBlock = ^{
NSLog(@"A"); // 任务A
sleep(3); // 任务B
NSLog(@"C"); // 任务C
};
[self.navigationController popViewControllerAnimated:true]; // 任务D
dispatch_async(dispatch_get_main_queue(), ^{
self.myBlock();
});
}
那么我们再修改一下代码:
- (IBAction)btnClick2:(UIButton *)sender {
self.myBlock = ^{
NSLog(@"A"); // 任务A
sleep(3); // 任务B
NSLog(@"C"); // 任务C
};
[self.navigationController popViewControllerAnimated:true]; // 任务D
self.myBlock();
}
那么问题又来了,现在的任务调用顺序是什么呢?(正解:A B C D)
理由:上面两段代码,其区别在于 block 是否是异步调用的。而我们理解这两段程序首先要明白一点,那就是 pop 方法是异步调用的。如果 pop 不是异步调用的,那么你怎么可能在 pop 执行过后再去拿到 self 指针去调用其他的方法呢?pop 过后,self 可就出栈了,那样一来在 pop 之后调用 self 指针不就会报错了吗。
所以说第一段代码 pop 和 block 都是异步调用的,所以按照顺序执行。主线程中原本包含任务 btnClick2,所以按照顺序执行完 btnClick2 就可以了,而 pop 也不会受到影响(这里指卡顿)。第二段代码由于 pop 异步,并且主线程会将 pop 异步放在最后执行。那么主线程在执行 btnClick2 这个任务时,就会先执行完 myBlock 的赋值,然后将 pop 异步放在最后调用,再接着执行 myBlock 的代码,再接着执行 btnClick2 的右花括号。这样一来 btnClick2 任务就完全执行完毕,主线程空闲,此时再执行 pop 任务。所以说运行结果会先打印A,然后卡顿3s,然后打印C,最后再 pop。此时会造成 pop 的卡顿。
第二个 demo,有助于加深理解:
- (IBAction)btnClick2:(UIButton *)sender {
dispatch_queue_t queue = dispatch_queue_create("darktest", nil);
NSLog(@"doSomething 1 %@", [NSThread currentThread]);
NSLog(@"doSomething 2 %@", [NSThread currentThread]);
dispatch_async(queue, ^{
NSLog(@"doSomething 3 %@", [NSThread currentThread]);
});
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"doSomething 4 %@", [NSThread currentThread]);
});
NSLog(@"doSomething 5 %@", [NSThread currentThread]);
}
那么上面的这 5 句打印,是以什么顺序执行的?
我们先为这段代码加上注释:
- (IBAction)btnClick2:(UIButton *)sender {
dispatch_queue_t queue = dispatch_queue_create("darktest", nil); // 串行队列
// 主线程执行
NSLog(@"doSomething 1 %@", [NSThread currentThread]);
NSLog(@"doSomething 2 %@", [NSThread currentThread]);
// 添加到不同队列的任务,async 可以开线程立即执行,主线程不等待此任务执行完
dispatch_async(queue, ^{
NSLog(@"doSomething 3 %@", [NSThread currentThread]);
});
// 主队列任务,async 不开线程,任务加入到主队列后面,主线程不等待此任务执行完
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"doSomething 4 %@", [NSThread currentThread]);
});
// 主线程执行
NSLog(@"doSomething 5 %@", [NSThread currentThread]);
}
由于在主线程中顺序执行,所以 1 - 2 是首要先执行的,然后 3 会开线程并且启用全新的串行队列,所以 3 立即执行,但 3 开线程需要耗时。由于 4 主线程主队列异步等待执行,而 5 是主线程主队列正常顺序执行,所以 5 会在 4 之前执行。
所以得出结论:
1 2 肯定先顺序打印,3 4 5 均在 1 2 后面打印,4 一定在 5 之后打印,3 不一定在 1 2 之后的哪个位置打印。
第三个 demo,相信这个 demo 过后你就会对 异步/同步/串行/并发 理解的更加透彻了:
- (IBAction)btnClick2:(UIButton *)sender {
dispatch_queue_t queue = dispatch_queue_create("darktest", nil);
NSLog(@"doSomething 1 %@", [NSThread currentThread]);
NSLog(@"doSomething 2 %@", [NSThread currentThread]);
dispatch_sync(queue, ^{
NSLog(@"doSomething 2.5 %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"doSomething 3 %@", [NSThread currentThread]);
dispatch_async(queue, ^{
NSLog(@"doSomething 6 %@", [NSThread currentThread]);
});
NSLog(@"doSomething 7 %@", [NSThread currentThread]);
});
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"doSomething 4 %@", [NSThread currentThread]);
});
NSLog(@"doSomething 5 %@", [NSThread currentThread]);
}
那么上面的这 8 句打印,是以什么顺序执行的?
我们还是先为这段代码加上注释:
- (IBAction)btnClick2:(UIButton *)sender {
dispatch_queue_t queue = dispatch_queue_create("darktest", nil); // 串行队列
// 主线程执行
NSLog(@"doSomething 1 %@", [NSThread currentThread]);
NSLog(@"doSomething 2 %@", [NSThread currentThread]);
// 主线程等待sync完成任务再继续执行70行往下的代码。由于GCD优化,sync的任务会被当前线程(主线程)执行。
dispatch_sync(queue, ^{
NSLog(@"doSomething 2.5 %@", [NSThread currentThread]);
});
// 添加到不同队列的任务,async可以开线程立即执行,主线程不等待此任务执行完
dispatch_async(queue, ^{
// =====此时是子线程在执行代码=====
NSLog(@"doSomething 3 %@", [NSThread currentThread]);
// 添加到同一个串行队列的异步任务,不开线程,任务加入到串行队列后面,当前线程不等待任务执行完
dispatch_async(queue, ^{
NSLog(@"doSomething 6 %@", [NSThread currentThread]);
});
// 当前线程先完成当前任务
NSLog(@"doSomething 7 %@", [NSThread currentThread]);
});
// 主队列任务,async不开线程,任务加入到主队列后面,主线程不等待此任务执行完
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"doSomething 4 %@", [NSThread currentThread]);
});
// 主线程执行
NSLog(@"doSomething 5 %@", [NSThread currentThread]);
}
1 2 2.5 肯定率先执行,(3 6 7)在新的线程中执行,(3 6 7)整体顺序未知,5 肯定在 4 的前面执行。(3 6 7)内部肯定是 3 7 6 的顺序执行。
然后我们将 6 的地方改为:
dispatch_sync(queue, ^{
NSLog(@"doSomething 6 %@", [NSThread currentThread]);
});
那么结果会是怎样的?
直接得出结果:
主线程正常,而子线程死锁,App 还是崩溃了。
第四个 demo,如果你分析对了调用顺序,基本上这一部分就理解了:
- (IBAction)btnClick2:(UIButton *)sender {
dispatch_queue_t queue = dispatch_queue_create("darktest", nil); //串行队列
dispatch_queue_t queue2 = dispatch_queue_create("darktest2", nil); //串行队列2
//主线程执行
NSLog(@"doSomething 1 %@", [NSThread currentThread]);
dispatch_sync(queue, ^{ // A
NSLog(@"doSomething 2 %@", [NSThread currentThread]);
dispatch_sync(queue2, ^{ // B
NSLog(@"doSomething 3 %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{ // C
NSLog(@"doSomething 4 %@", [NSThread currentThread]);
dispatch_sync(dispatch_get_main_queue(), ^{ // D
NSLog(@"doSomething 5 %@", [NSThread currentThread]);
dispatch_sync(queue2, ^{ // E
NSLog(@"doSomething 6 %@", [NSThread currentThread]);
});
NSLog(@"doSomething 7 %@", [NSThread currentThread]);
});
NSLog(@"doSomething 8 %@", [NSThread currentThread]);
});
NSLog(@"doSomething 9 %@", [NSThread currentThread]);
});
//主线程执行
NSLog(@"doSomething 10 %@", [NSThread currentThread]);
}
其中 4 的打印位置不确定,如果你也得到的这个结果,那么恭喜你。在这里不做解释。不明白的可以留言。
最后得出我们本文的结论:
异步、同步、串行、并发 这些名词的概念都不能脱离 线程、队列 来单独描述。单纯去解释一个名词在我看来是没有意义的。大家通过 demo 可以理解了就好。
本文感谢:darkhandz 提供 demo,感谢我俩两天如同浆糊般的讨论。