美文网首页iOS
iOS底层探索之多线程(十一)—GCD源码分析(调度组)

iOS底层探索之多线程(十一)—GCD源码分析(调度组)

作者: 俊而不逊 | 来源:发表于2021-08-20 08:57 被阅读0次

    回顾

    在上篇博客已经对GCD信号量做了一个介绍和举例应用,还有对底层源码的分析,那么本篇博客 看苹果工程师,如何巧妙封装调度组,看完底层源码直呼好家伙,真是妙啊!!!

    多线程

    iOS底层探索之多线程(一)—进程和线程

    iOS底层探索之多线程(二)—线程和锁

    iOS底层探索之多线程(三)—初识GCD

    iOS底层探索之多线程(四)—GCD的队列

    iOS底层探索之多线程(五)—GCD不同队列源码分析

    iOS底层探索之多线程(六)—GCD源码分析(sync 同步函数、async 异步函数)

    iOS底层探索之多线程(七)—GCD源码分析(死锁的原因)

    iOS底层探索之多线程(八)—GCD源码分析(函数的同步性、异步性、单例)

    iOS底层探索之多线程(九)—GCD源码分析(栅栏函数)

    iOS底层探索之多线程(十)—GCD源码分析( 信号量)

    1. 调度组

    1.1 调度组介绍

    调度组最直接的作用就是控制任务的执行顺序

    • dispatch_group_create :创建调度组组
    • dispatch_group_async:进组的任务 执行
    • dispatch_group_notify :进组任务执行完毕的通知
    • dispatch_group_wait: 进组任务执行等待时间
    • dispatch_group_enter :任务进组
    • dispatch_group leave :任务出组

    1.2 调度组举例

    下面举个调度组的应用举例

    给图片添加水印,有两张水印照片需要网络请求,水印照片请求,完成之后,再添加到本地图片上面显示!

    //创建调度组
        dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        // 水印 1
        dispatch_group_async(group , queue, ^{
            NSString *logoStr1 = @"https://thirdqq.qlogo.cn/g?b=sdk&k=zeIp1PmCE6jff6BGSbjicKQ&s=140&t=1556562300";
            NSData *data1 = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr1]];
            UIImage *image1 = [UIImage imageWithData:data1];
            [self.mArray addObject:image1];
        });
        // 水印 1
        dispatch_group_async(group , queue, ^{
            NSString *logoStr2 = @"https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTJKuHEuLLyYK0Rbw9s9G8jpcnMzQCNsuYJRIRjCvltH6NibibtP73EkxXPR9RaWGHvmHT5n69wpKV2w/132";
            NSData *data2 = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr2]];
            UIImage *image2 = [UIImage imageWithData:data2];
            [self.mArray addObject:image2];
        });
        // 水印请求完成
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            UIImage *newImage = nil;
            NSLog(@"请求完毕,添加水印");
            for (int i = 0; i<self.mArray.count; i++) {
                UIImage *waterImage = self.mArray[i];
                newImage =[JP_ImageTool jp_WaterImageWithWaterImage:waterImage backImage:newImage waterImageRect:CGRectMake(20, 100*(i+1), 120, 60)];
            }
            self.imageView.image = newImage;
    
        });
    
    • 添加水印前
    模拟器运行结果—添加水印前
    • 添加水印后
    模拟器运行结果—添加水印后
    当组内的任务全部执行完成了,dispatch_group_notify会通知,任务已经完成了,内部添加水印的工作可以开始了!

    上面的例子还可以使用dispatch_group_enterdispatch_group leave 搭配使用,如下:

    进组和出组搭配使用.gif

    从上面的两个例子代码可以发现,dispatch_group_async相当于是dispatch_group_enter + dispatch_group leave 的作用!

    注意dispatch_group_enterdispatch_group leave 搭配使用,但是顺序不能反,否则会奔溃,如下:

    奔溃截图
    dispatch_group_enterdispatch_group leave 搭配使用,除了顺序不发,个数也得保持一致,人家是出入成双成对,你不能把它们分开,否则也会罢工或者奔溃的!
    • dispatch_group_enter进组不出组情况
    进组不出组情况

    dispatch_group_enter进组不出组,那么dispatch_group_notify就不会收到任务执行完成的通知,dispatch_group_notify内的任务就执行不了

    • 不进组就出组 dispatch_group leave 情况
    不进组就出组

    不进组就出组,程序会奔溃,都没有任务进去,你去出去,出个锤子哦!😢

    • dispatch_group_wait等待 举例
    dispatch_group_wait举例
    dispatch_group_wait有点栅栏的感觉,堵住了组里面前面的任务,但是并没有阻塞主线程。那么再看看下面这个例子 dispatch_group_wait举例
    • 这里使用了dispatch_group_wait进行等待
    • dispatch_group_wait函数会一直等到前面group中的任务执行完,再执行下面代码,但会产生阻塞线程的问题,导致了主线程中的任务5不能正常运行,直到任务组的任务完成才能被调用。

    思考

    1. 那么调度组是如何工作,为什么可以调度任务呢?
    2. dispatch_group_enter进组和dispatch_group_leave出组为什么能够起到与调度组dispatch_group_async一样的效果呢?

    现在去看看源码寻找答案!

    2. 调度组源码分析

    2.1 dispatch_group_create

    • dispatch_group_create
    dispatch_group_t
    dispatch_group_create(void)
    {
        return _dispatch_group_create_with_count(0);
    }
    

    创建调度组会调用_dispatch_group_create_with_count方法,并默认传入0

    • _dispatch_group_create_with_count
    static inline dispatch_group_t
    _dispatch_group_create_with_count(uint32_t n)
    {
        dispatch_group_t dg = _dispatch_object_alloc(DISPATCH_VTABLE(group),
                sizeof(struct dispatch_group_s));
        dg->do_next = DISPATCH_OBJECT_LISTLESS;
        dg->do_targetq = _dispatch_get_default_queue(false);
        if (n) {
            os_atomic_store2o(dg, dg_bits,
                    (uint32_t)-n * DISPATCH_GROUP_VALUE_INTERVAL, relaxed);
            os_atomic_store2o(dg, do_ref_cnt, 1, relaxed); // <rdar://22318411>
        }
        return dg;
    }
    

    _dispatch_group_create_with_count方法里面通过os_atomic_store2o来把传入的 n进行保存,这里的写法和信号量很像(如下图),是模仿的信号量的写法自己写了一个,但并不是调度组底层是使用信号量实现的。

    dispatch_semaphore_create

    2.2 dispatch_group_enter

    • dispatch_group_enter
    dispatch_group_enter
    通过os_atomic_sub_orig2o会进行0的减减操作,此时的old_bits等于-1

    2.3 dispatch_group_leave

    • dispatch_group_leave
    dispatch_group_leave
    这里通过os_atomic_add_orig2o-1加加操作,old_state就等于00 & DISPATCH_GROUP_VALUE_MASK的结果依然等于0,也就是old_value等于0DISPATCH_GROUP_VALUE_1的定义如下代码:
    DISPATCH_GROUP_VALUE_1
    从代码中可以看出old_value是不等于DISPATCH_GROUP_VALUE_MASK的,所以代码会执行到外面的if中去,并调用_dispatch_group_wake方法进行唤醒,唤醒的就是dispatch_group_notify方法。

    也就是说,如果不调用dispatch_group_leave方法,也就不会唤醒dispatch_group_notify,下面的流程也就不会执行了。

    2.4 dispatch_group_notify

    • dispatch_group_notify
    dispatch_group_notify
    old_state等于0的情况下,才会去唤醒相关的同步或者异步函数的执行,也就是block里面的执行,就是调用同步、异步的那个callout执行。
    • dispatch_group_leave分析中,我们已经得到old_state结果等于0
    • 所以这里也就解释了dispatch_group_enterdispatch_group_leave为什么要配合起来使用的原因,通过信号量的控制,避免异步的影响,能够及时唤醒并调用dispatch_group_notify方法
    • dispatch_group_leave里面也有调用_dispatch_group_wake方法,这是因为异步的执行,任务是执行耗时的,有可能dispatch_group_leave这行代码还没有走,就先走了dispatch_group_notify方法,但这时候dispatch_group_notify方法里面的任务并不会执行,只是把任务添加到 group
    • 它会等dispatch_group_leave执行了被唤醒才执行,这样就保证了异步时,dispatch_group_notify里面的任务不丢弃,可以正常执行。如下图所示:
      示意图
    • 当执行任务 2的时候,是耗时任务(sleep(5)模拟耗时),异步不会堵塞,会执行后面的代码,就是图中①,dispatch_group_notify里面的任务会包装起来,进group
    • 包装完成,异步执行完,这时候就走 ②了,又回到dispatch_group_leave处去执行了,这时候就可以通过 group 拿到任务 4,直接去调用_dispatch_group_wake任务 4唤醒执行了。
    • 这一波是非常的细节,苹果工程师真是秒啊!


      苹果工程师牛逼

    2.5 dispatch_group_async

    猜想dispatch_group_async里面应该是封装了dispatch_group_enterdispatch_group_leave,所以才能起到一样的作业效果!

    • dispatch_group_async
    dispatch_group_async

    dispatch_continuation_t的处理,也就是任务的包装处理,还做了一些标记处理,最后走_dispatch_continuation_group_async

    • _dispatch_continuation_group_async
    _dispatch_continuation_group_async
    靓仔!看到没有,和猜想的是一样的,内部果然封装了dispatch_group_enter方法,向组中添加任务时,就调用了dispatch_group_enter方法,将信号量0变成了-1。那么现在去找下dispatch_group_leave的在哪里!继续跟踪流程。。。
    • _dispatch_continuation_async
    _dispatch_continuation_async
    这一波又是非常的熟悉了,这个dx_push我们都已经非常熟悉了,异步、同步的时候经常见这个方法,这里就不再赘述了(传送门),会调用_dispatch_root_queue_push -- > _dispatch_root_queue_push_inline -- > _dispatch_root_queue_poke -- > _dispatch_root_queue_poke_slow -- > _dispatch_root_queues_init -- > _dispatch_root_queues_init_once -- > _dispatch_worker_thread2 -- > _dispatch_root_queue_drain
    _dispatch_root_queue_drain
    然后_dispatch_root_queue_drain -- > _dispatch_continuation_pop_inline -- > _dispatch_continuation_with_group_invoke _dispatch_continuation_with_group_invoke

    长舒一口气,哎呀我的妈呀!终于把流程跟踪完成了😅

    在最后_dispatch_continuation_with_group_invoke里面我们找到了出组的方法dispatch_group_leave
    在这里完成_dispatch_client_callout函数调用,紧接着调用dispatch_group_leave方法,将信号量由-1变成了0

    至此完成闭环,完整的分析了调度组、进组、出组、通知的底层原理和关系。

    此处应该有掌声,45 度仰望天花板,我这该死无处安放的魅力!


    鼓掌,好,好,好 !

    3. 总结

    • 调度组最直接的作用就是控制任务的执行顺序

    • dispatch_group_notify :进组任务执行完毕的通知

    • dispatch_group_wait函数会一直等到前面group中的任务执行完,后面的才可以执行

    • dispatch_group_enterdispatch_group leave 成对使用

    • dispatch_group_async内部封装了dispatch_group_enterdispatch_group leave 的使用

    更多内容持续更新

    🌹 喜欢就点个赞吧👍🌹

    🌹 觉得有收获的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹

    🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹

    相关文章

      网友评论

        本文标题:iOS底层探索之多线程(十一)—GCD源码分析(调度组)

        本文链接:https://www.haomeiwen.com/subject/nogzbltx.html