[OC | GCD] dispatch_group_notify can not notify dispatch_get_main_queue(调度组无法通知主队列)的问题

调度组无法通知主队列的问题

写在前面

通过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;
    }
}

再运行一遍:
runloopObserver
呦吼,发现了什么?没错,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中执行。

猜你喜欢

转载自blog.csdn.net/qq_41749924/article/details/125345066