美文网首页
GCD的探索三两事

GCD的探索三两事

作者: 佛祖ohmygod | 来源:发表于2021-04-09 21:11 被阅读0次

    GCD

    GCD优势

    • GCD是苹果公司为多核的并行运算提出的解决方案

    • GCD会自动利用更多的CPU内核(比如双核、四核)

    • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)

    • 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
      **
      【重点】用一句话总结GCD就是:将任务添加到队列,并指定任务执行的函数
      **

    GCD核心

    dispatch_async( dispatch_queue_create("com.CJL.Queue", NULL), {

    NSLog(@"GCD基本使用");

    });

    将上述代码拆分,方便我们来理解GCD的核心 主要是由 任务 + 队列 + 函数 构成

    //********GCD基础写法********
    //创建任务
    dispatch_block_t block = {
    NSLog(@"hello GCD");
    };

    //创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("com.CJL.Queue", NULL);

    //将任务添加到队列,并指定函数执行
    dispatch_async(queue, block);

    函数
    在GCD中执行任务的方式有两种,同步执行和异步执行,分别对应 同步函数dispatch_sync异步函数dispatch_async,两者对比如下

    同步执行,对应同步函数dispatch_sync
    必须等待当前语句执行完毕,才会执行下一条语句

    不会开启线程,即不具备开启新线程的能力

    在当前线程中执行block任务

    异步执行,对应异步函数dispatch_async
    不用等待当前语句执行完毕,就可以执行下一条语句

    会开启线程执行block任务,即具备开启新线程的能力(但并不一定开启新线程,这个与任务所指定的队列类型有关)

    异步 是 多线程 的代名词

    所以,综上所述,两种执行方式的主要区别有两点:

    是否等待队列的任务执行完毕

    是否具备开启新线程的能力

    主队列&全局并发队列

    底层都是静态结构体

    队列的创建
    dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
    {

    image.jpeg


    }

    // dqai 创建 - 属性的设置串行还是并发
    dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);


    image.jpeg

    _dispatch_object_alloc 通过这个创建队列,队列也是一个对象


    image.jpeg image.jpeg

    异步函数的创建

    image.jpeg image.jpeg

    异步函数
    所以,综上所述,异步函数的底层分析如下

    【准备工作】:首先,将异步任务拷贝并封装,并设置回调函数func

    【block回调】:底层通过dx_push递归,会重定向到根队列,然后通过pthread_creat创建线程,最后通过dx_invoke执行block回调(注意dx_push 和 dx_invoke 是成对的)

    同步函数

    进入dispatch_sync源码实现,其底层的实现是通过栅栏函数实现的

    同步函数 + 并发队列 顺序执行的原因

    在_dispatch_sync_invoke_and_complete -> _dispatch_sync_function_invoke_inline源码中,主要有三个步骤:

    将任务压入队列: _dispatch_thread_frame_push
    执行任务的block回调: _dispatch_client_callout
    将任务出队:_dispatch_thread_frame_pop
    从实现中可以看出,是先将任务push队列中,然后执行block回调,在将任务pop,所以任务是顺序执行的。

    同步函数的底层实现如下:

    同步函数的底层实现实际是同步栅栏函数

    <u>同步函数中如果当前正在执行的队列和等待的是同一个队列,形成相互等待的局面,则会造成死锁</u>

    单例

    只能进去一次,why?

    参数1:onceToken,它是一个静态变量,由于不同位置定义的静态变量是不同的,所以静态变量具有唯一性

    进入dispatch_once_f源码,其中的val 是外界传入的onceToken静态变量,而func是_dispatch_Block_invoke(block),其中单例的底层主要分为以下几步

    • 将val,也就是静态变量转换为dispatch_once_gate_t类型的变量l

    • 通过os_atomic_load获取此时的任务的标识符v

    • 如果v等于DLOCK_ONCE_DONE,表示任务已经执行过了,直接return

    • 如果 任务执行后,加锁失败了,则走到_dispatch_once_mark_done_if_quiesced函数,再次进行存储,将标识符置为DLOCK_ONCE_DONE

    • 反之,则通过_dispatch_once_gate_tryenter尝试进入任务,即解锁,然后执行_dispatch_once_callout执行block回调

    • 如果此时有任务正在执行,再次进来一个任务2,则通过_dispatch_once_wait函数让任务2进入无限次等待

    ** _dispatch_once_callout 回调**
    进入_dispatch_once_callout源码,主要就两步

    _dispatch_client_callout:block回调执行

    _dispatch_once_gate_broadcast:进行广播

    针对单例的底层实现,主要说明如下:
    【单例只执行一次的原理】:GCD单例中,有两个重要参数,onceToken 和 block,其中onceToken是静态变量,具有唯一性,在底层被封装成了dispatch_once_gate_t类型的变量l,l主要是用来获取底层原子封装性的关联,即变量v,通过v来查询任务的状态,如果此时v等于DLOCK_ONCE_DONE,说明任务已经处理过一次了,直接return

    【block调用时机】:如果此时任务没有执行过,则会在底层通过C++函数的比较,将任务进行加锁,即任务状态置为DLOCK_ONCE_UNLOCK,目的是为了保证当前任务执行的唯一性,防止在其他地方有多次定义。加锁之后进行block回调函数的执行,执行完成后,将当前任务解锁,将当前的任务状态置为DLOCK_ONCE_DONE,在下次进来时,就不会在执行,会直接返回

    【多线程影响】:如果在当前任务执行期间,有其他任务进来,会进入无限次等待,原因是当前任务已经获取了锁,进行了加锁,其他任务是无法获取锁的

    栅栏函数 -栏的是当前队列

    栅栏函数一般用并发队列,因为串行队列底层本来就有栅栏函数

    栅栏函数不能和全局并发队列一起使用,因为全局并发队列只有一条,如果拦着的话,那其他用到全局并发队列也被阻拦了

    同步栅栏函数dispatch_barrier_sync(在主线程中执行):前面的任务执行完毕才会来到这里,但是同步栅栏函数会堵塞线程,影响后面的任务执行

    异步栅栏函数dispatch_barrier_async:前面的任务执行完毕才会来到这里

    栅栏函数最直接的作用就是 控制任务执行顺序,使同步执行

    同时,栅栏函数需要注意一下几点

    栅栏函数只能控制同一并发队列

    同步栅栏添加进入队列的时候,当前线程会被锁死,直到同步栅栏之前的任务和同步栅栏任务本身执行完毕时,当前线程才会打开然后继续执行下一句代码。

    在使用栅栏函数时.使用自定义队列才有意义,如果用的是串行队列或者系统提供的全局并发队列,这个栅栏函数的作用等同于一个同步函数的作用,没有任何意义

    * 异步栅栏函数阻塞的是队列,而且必须是自定义的并发队列,不影响主线程任务的执行

    同步栅栏函数阻塞的是线程,且是主线程,会影响主线程其他任务的执行 *

    如果栅栏函数中使用 全局队列, 运行会崩溃,原因是系统也在用全局并发队列,使用栅栏同时会拦截系统的,所以会崩溃

    如果将自定义并发队列改为串行队列,即serial ,串行队列本身就是有序同步 此时加栅栏,会浪费性能

    栅栏函数只会阻塞一次

    ** 栅栏函数原理流程**
    1、通过_dispatch_tid_self获取线程ID
    2、通过_dispatch_queue_try_acquire_barrier_sync判断线程状态
    3、通过_dispatch_sync_recurse递归查找栅栏函数的target
    4、通过_dispatch_introspection_sync_begin对向前信息进行处理
    5、通过_dispatch_lane_barrier_sync_invoke_and_complete执行block并释放

    信号量

    信号量的作用一般是用来使任务同步执行,类似于互斥锁,用户可以根据需要控制GCD最大并发数,一般是这样使用的
    dispatch_semaphore_wait 加锁
    该函数的源码实现如下,其主要作用是对信号量dsema通过os_atomic_dec2o进行了--操作,其内部是执行的C++的atomic_fetch_sub_explicit方法

    如果value 大于等于0,表示操作无效,即执行成功

    如果value 等于LONG_MIN,系统会抛出一个crash

    如果value 小于0,则进入长等待

    dispatch_semaphore_create 主要就是初始化限号量

    dispatch_semaphore_wait是对信号量的value进行--,即加锁操作

    dispatch_semaphore_signal 是对信号量的value进行++,即解锁操作

    dispatch_semaphore_signal 解锁
    该函数的源码实现如下,其核心也是通过os_atomic_inc2o函数对value进行了++操作,os_atomic_inc2o内部是通过C++的atomic_fetch_add_explicit

    如果value 大于 0,表示操作无效,即执行成功

    如果value 等于0,则进入长等待
    总结
    dispatch_semaphore_create 主要就是初始化限号量

    dispatch_semaphore_wait是对信号量的value进行--,即加锁操作

    dispatch_semaphore_signal 是对信号量的value进行++,即解锁操作

    image.jpeg

    调度组

    调度组的最直接作用是控制任务执行顺序,常见方式如下

    ** dispatch_group_t**
    dispatch_group_t group = dispatch_group_create();

    dispatch_group_enter(group); /
    dispatch_group_leave(group);/

    这两个只要成对出现,那么久可以通知调度组了
      dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"他们回来了 我准备主线程更新UI");
    });
    
    只要有enter-leave成对匹配,notify就会执行,不会等两个组都执行完。意思就是只要enter-leave成对就可以执行
    
    

    /*
    【修改二】再加一个enter,即enter:wait 是 3:2,能否执行notify?

    image.jpeg


    不能,会一直等待,等一个leave,才会执行notify
    */

    【修改三】如果是 enter:wait 是 2:3,能否执行notify?

    image.jpeg

    会崩溃,因为enter-leave不成对,崩溃在里面是因为async有延迟

    dispatch_group_async = enter - leave

    dispatch_group_create ->
    _dispatch_group_create_with_count->


    image.jpeg

    1、调度组dispatch_group_create刚创建的时候,状态标记为0,当有一个enter的时候,状态-1,当有leave的时候,状态+1,这时候就有变为0,然后唤醒dispatch_group_t,进行通知notify
    2、如果enter和leave不成对出现,只有enter的时候,dispatch_group_t是-1状态,会一直睡眠等待唤醒,但是如果leave比enter多的时候,状态是正的+1,这时候底层就会崩溃,抛出异常

    dispatch_group_enter 进组
    进入dispatch_group_enter源码,通过os_atomic_sub_orig2o对dg->dg.bits 作 --操作,对数值进行处理

    image.jpeg

    dispatch_group_leave 出组
    进入dispatch_group_leave源码
    -1 到 0,即++操作
    根据状态,do-while循环,唤醒执行block任务
    如果0 + 1 = 1,enter-leave不平衡,即leave多次调用,会crash

    image.jpeg

    进入_dispatch_group_wake源码,do-while 循环进行异步命中,调用_dispatch_continuation_async执行

    image.jpeg

    dispatch_group_notify 通知
    进入dispatch_group_notify源码,如果old_state等于0,就可以进行释放了

    image.jpeg

    除了leave可以通过_dispatch_group_wake唤醒,其中dispatch_group_notify也是可以唤醒的

    dispatch_group_async
    进入dispatch_group_async 源码,主要是包装任务和异步处理任务

    image.jpeg

    进入_dispatch_continuation_group_async源码,主要是封装了dispatch_group_enter进组操作


    image.jpeg

    进入_dispatch_continuation_async源码,执行常规的异步函数底层操作。既然有了enter,肯定有leave,我们猜测block执行之后隐性的执行leave,

    搜索_dispatch_client_callout的调用,在_dispatch_continuation_with_group_invoke中


    image.jpeg

    总结

    • enter-leave只要成对就可以,不管远近

    • dispatch_group_enter在底层是通过C++函数,对group的value进行--操作(即0 -> -1)

    • dispatch_group_leave在底层是通过C++函数,对group的value进行++操作(即-1 -> 0)

    • dispatch_group_notify在底层主要是判断group的state是否等于0,当等于0时,就通知

    • block任务的唤醒,可以通过dispatch_group_leave,也可以通过dispatch_group_notify

    • dispatch_group_async 等同于enter - leave,其底层的实现就是enter-leave

    ** dispatch_source**

    相关文章

      网友评论

          本文标题:GCD的探索三两事

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