写在前面
通过dispatch_group来解决依赖问题是个经典的方案。
通常会在dispatch_group_notify中去执行更新UI的操作。
而更新UI操作必须放在主队列中。
今天本来想给同事演示一下以上的用法,给他打完下面这一段发了过去,
过了一会同事过来弱弱的说好像哪里不对劲,
我说我赌上我500度的眼镜片,肯定是你哪里敲错了,
感觉他抱着必死的决心说一个字一个字敲的,真的不对,
我还就不信这个邪,跟着他到他工位上一探究竟。
情景复现
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("cQueue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"开始");
dispatch_group_async(group, queue, ^{
NSLog(@"任务一");
});
dispatch_group_async(group, queue, ^{
NSLog(@"任务二");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"任务已全部完成");
});
NSLog(@"已阻塞");
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"解开阻塞");
上面就是我给他的代码,先思考一下这段代码会输出什么?
大家一定会脱口而出:
开始
已阻塞
任务一
任务二
解开阻塞
任务已全部完成
然而真正的输出却是这样的:
2022-06-18 11:38:29.246415+0800 多线程[23769:10338520] 开始
2022-06-18 11:38:29.246991+0800 多线程[23769:10338520] 已阻塞
2022-06-18 11:38:29.247007+0800 多线程[23769:10338756] 任务一
2022-06-18 11:38:29.247027+0800 多线程[23769:10338757] 任务二
2022-06-18 11:38:29.247064+0800 多线程[23769:10338520] 解开阻塞
Program ended with exit code: 0
What?怎么少了一个,dispatch_group_nofity中的输出呢?先假装淡定,先尝试能不能搞出正确结果,主线程不够那就子线程来凑,把
dispatch_group_notify中改为子队列看看:
dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务已全部完成");
});
再运行一次
2022-06-18 13:25:11.230595+0800 多线程[29831:10437617] 开始
2022-06-18 13:25:11.231078+0800 多线程[29831:10437617] 已阻塞
2022-06-18 13:25:11.231104+0800 多线程[29831:10437935] 任务一
2022-06-18 13:25:11.231111+0800 多线程[29831:10437936] 任务二
2022-06-18 13:25:11.231146+0800 多线程[29831:10437936] 任务已全部完成
2022-06-18 13:25:11.231148+0800 多线程[29831:10437617] 解开阻塞
Program ended with exit code: 0
呕吼,它来了它来了,notify带着子线程走来了。看到这儿我首先想到的是死锁,但是立马否定,这跟死锁八竿子打不着,那还会是什么问题呢?我迅速在脑海中过了一遍主线程和子线程的区别:
1.主线程只有一条,子线程可以有N条。
2.UI和main函数只能在主线程运行。
3.主线程的生命周期是贯穿整个程序,子线程可以自由开关。
4.主线程的runloop一开始就打开,子线程的runloop需要自己打开。
等等,runloop? 这个类似于while死循环的东西,可掌握整个程序的生命周期,再回头看一下刚刚的输出,包含了一句:
Program ended with exit code: 0
挖丢,程序咋退出了呢?再定睛一看,原来同事用的是终端程序跑的代码,这是一个突破点,再想想UI程序能够一直存在的关键所在:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
}
//这一句可以在程序启动时,自动启动主线程的runloop
return NSApplicationMain(argc, argv);
}
解决
所以终端程序默认是没有启动主线程的runloop的,可是我明明将任务塞到主队列里了呀,为什么不等主队列执行完才结束程序呢?
先带着大大的疑惑,将主线程的runloop开启,模拟UI程序试试:
int main(int argc, const char * argv[]) {
[GCD_Demo main];
[[NSRunLoop currentRunLoop] run];
return 0;
}
2022-06-18 13:48:42.107653+0800 多线程[31563:10471756] 开始
2022-06-18 13:48:42.108093+0800 多线程[31563:10471756] 已阻塞
2022-06-18 13:48:42.108122+0800 多线程[31563:10472055] 任务一
2022-06-18 13:48:42.108150+0800 多线程[31563:10472056] 任务二
2022-06-18 13:48:42.108216+0800 多线程[31563:10471756] 解开阻塞
2022-06-18 13:48:42.108307+0800 多线程[31563:10471756] 任务已全部完成
果然达到了想要的效果,dispatch_group_nofity中的信息打印出来了,感觉离答案越来越近了,看来是跟runloop有关系了,既然如此,先去看看runloop的打印状态,看能不能找出蛛丝马迹:
+ (void)observer {
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting | kCFRunLoopAfterWaiting, YES, 0, observeRunLoopActivities, NULL);
// 添加observer到Runloop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);
}
void observeRunLoopActivities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
NSLog(@"default");
break;
}
}
再运行一遍:
呦吼,发现了什么?没错,AfterWaiting,这是在runloop休眠后重新启动的信号,说明这已经是runloop的第二轮了。
得出结论:
dispatch_group_nofity中加入主队列,是将任务添加到主队列的下一轮runloop中,而终端程序是一次性的程序,所以它等不到主线程的第二次执行,就像我再听不到那一年夏天的蝉鸣。
拓展
至此这个问题已经有了结论,但是我们不禁想到,主队列dispatch_get_main_queue()的使用场景可不止dispatch_group_notify,在dispatch_async和dispatch_sync中也是非常常用的,结论还适用吗?
话不多说,代码验证:
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"dispatch_async");
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"dispatch_sync");
});
});
不要问为什么dispatch_sync嵌套一层。。问就是死锁 —。—!
2022-06-18 14:11:48.154426+0800 多线程[33514:10504920] kCFRunLoopBeforeSources
2022-06-18 14:11:48.154835+0800 多线程[33514:10504920] kCFRunLoopAfterWaiting
2022-06-18 14:11:48.154855+0800 多线程[33514:10504920] dispatch_async
2022-06-18 14:11:48.154868+0800 多线程[33514:10504920] dispatch_sync
2022-06-18 14:11:48.154882+0800 多线程[33514:10504920] kCFRunLoopBeforeSources
结果显而易见,同样是加到了runloop下一次循环中,所以上面的结论同样适用,
结论
主动往主队列中添加任务,这些任务会在下一次runloop中执行。