GCD的探索三两事

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,所以任务是顺序执行的。

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

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

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

单例

只能进去一次,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的探索三两事)