美文网首页
GCD原理(下)

GCD原理(下)

作者: 北京_小海 | 来源:发表于2020-11-06 16:46 被阅读0次

    在上篇文章函数与队列和gcd原理分析(上)中我们分析了gcd原理,dispatch_async函数 下面继续讲解

    上篇分析了_dispatch_continuation_init进行了包装 咱们再来看看_dispatch_continuation_async

    我们知道了上一步对信息进行函数式封装,那么对于一个异步执行来说,最重要的就是何时创建线程和函数执行呢,那么就再这个方法里面了。

    _dispatch_continuation_async

    这个方法主要就是执行了dx_push方法,查看其代码,发现为宏定义,主要执行了dq_push方法.

    那么dq_push又是怎么赋值的呢,由于其是一个属性,所以我们可以搜索.dq_pus来查看其赋值。我们发现其赋值的地方非常多,但是大体的意思我们可以理解,就是主要在根队列,自定义队列,主队列等等进行push操作的时候调用。

    我们知道线程的创建一般都是在根队列上进行创建的,所以我们直接找根队列的dq_push赋值,这样比较快速,当然其他的也可以,因为递归的关系最终都会走到这里。

    我们发现_dispatch_root_queue_push方法最终会调用_dispatch_root_queue_push_inline方法,而_dispatch_root_queue_push_inline方法最终又会调用_dispatch_root_queue_poke。

    _dispatch_root_queue_poke这个函数主要进行了一些容错的判断,最终走到了_dispatch_root_queue_poke_slow相关的方法里

     _dispatch_root_queue_poke_slow

    这个方法就是异步执行的主要方法,创建线程也是在此,由于代码比较长,我们还是寻找代码中的关键节点来讲。

    到了这里可以清楚的看到对于全局队列使用 _pthread_workqueue_addthreads 开辟线程,对于其他队列使用 pthread_create 开辟新的线程。那么是如何调用执行的呢?其实 _dispatch_root_queues_init 中会首先执行第一个任务:

     dispatch_once_f我们后面分析单例会提到 这里看一下 _dispatch_root_queues_init_once

    先看下我们的堆栈

    调用_dispatch_worker_thread2 之后就按照堆栈顺序执行 最终进行了回调

    单例 dispatch_once

    通过dispatch_once函数查看其底层调用,可以发现其最终调用到dispatch_once_f方法中。相关的代码如下。

    首先我们知道val一开始为NULL,并将其转换为dispatch_once_gate_t

    通过查看_dispatch_once_gate_tryenter源码,我们知道其在OS底层通过判断l->dgo_once是否为DLOCK_ONCE_UNLOCKED状态

    如果成立,则会执行_dispatch_once_callout函数。执行对应的block,然后将l->dgo_once置为DLOCK_ONCE_DONE,从而保证了只执行一次

    // 如果 os_atomic_load 为 DLOCK_ONCE_DONE 则直接返回,否则进入

    _dispatch_once_gate_tryenter,在这里首先判断对象是否存储过,如果存储过则则标记为 unlock

    回调

    // 在 _dispatch_once_gate_broadcast 中由于执行完毕,使用_dispatch_once_mark_done 标记为 done

    栅栏函数

    dispatch_barrier_async(栅栏函数)他,通过其命名我们就知道是拦截的意思。也就是在栅栏函数之前的任务执行完成后,才能执行后边的任务。

    dispatch_barrier_sync 需要等待栅栏执行完才会执行栅栏后面的任务,而dispatch_barrier_async 无需等待栅栏执行完,会继续往下走(保留在队列里)

    再改成同步栅栏

    结论 dispatch_barrier_sync 需要等待栅栏执行完才会执行栅栏后面的任务,而dispatch_barrier_async 无需等待栅栏执行完,会继续往下走(保留在队列里)

    同步执行dispatch_sync

    死锁原因

    在_dispatch_sync_f_inline中发现了一个判断likely(dq->dq_width == 1,通过之前队列的原理我们可以知道,串行队列的width是为1的,所以串行的执行方法,是在_dispatch_barrier_sync_f中的。

    而且根据函数名,我们可以知道_dispatch_barrier是之前讲的栅栏函数的调用,所以说栅栏函数也会走到此方法中。

    最终,我们来到了_dispatch_barrier_sync_f_inline函数中。

    首先执行了_dispatch_tid_self方法。通过源码跟踪,我们可以发现其为宏定义的方法,底层主要执行了_dispatch_thread_getspecific。这个函数书主要是通过KeyValue的方式来获取线程的一些信息。在这里就是获取当前线程的tid,即唯一ID。

    我们知道,造成死锁的原因就是串行队列上任务的相互等待。那么必然会通过tid来判断是否满足条件,从而找到了_dispatch_queue_try_acquire_barrier_sync函数

    函数_dispatch_queue_try_acquire_barrier_sync_and_suspend中,从该函数我们可以知道,通过os_atomic_rmw_loop2o函数回调,从OS底层获取到了状态信息,并返回。

    那么返回之后,就执行了_dispatch_sync_f_slow函数。

    其中通过源码可以发现,首先是生成了一些任务的信息,然后通过_dispatch_trace_item_push来进行压栈操作,从而存放在我们的同步队列中(FIFO),从而实现函数的执行。

    那么产生死锁的主要检测就再__DISPATCH_WAIT_FOR_QUEUE__这个函数中了,通过查看函数,发现它会获取到队列的状态,看其是否为等待状态,然后调用_dq_state_drain_locked_by中的异或运算,判断队列和线程的等待状态,如果两者都在等待,那么就会返回YES,从而造成死锁的崩溃。

    死锁原因总结

    _dispatch_sync首先获取当前线程的tid

    获取到系统底层返回的status

    获取到队列的等待状态和tid比较,如果相同,则表示正在死锁,从而崩溃

    任务的执行

    对于同步任务的block执行,我们在继续跟进之前的源码_dispatch_sync源码中_dispatch_barrier_sync_f_inline函数,观看其函数实现,函数的执行主要是在_dispatch_client_callout方法中。

    查看_dispatch_client_callout方法,里面果然有函数的调用f(ctxt);

    至此,同步函数的block调用完成

    信号量 dispatch_semaphore

    1. dispatch_semaphore_create

    这个方法就是函数式保存,转换成了dispatch_semaphore_t对象。信号量的处理都是基于此对象来进行的。

    2.dispatch_semaphore_wait

    wait函数主要进行了3步操作:

    调用os_atomic_dec2o宏。通过对这个宏的查看,我们发现其就是一个对dsema进行原子性的-1操作

    判断value是否>= 0,如果满足条件,则不阻塞,直接执行

    调用_dispatch_semaphore_wait_slow。通过源码,我们可以发现其对timeout的参数进行了分别的处理

    _dispatch_semaphore_wait_slow函数的处理如下:

    default:主要调用了_dispatch_sema4_timedwait方法,这个方法主要是判断当前的操作是否超过指定的超时时间。

    DISPATCH_TIME_NOW中的while是一定会执行的,如果不满足条件,已经在之前的操作跳出了,不会执行到此。if操作调用os_atomic_cmpxchgvw2o,会将value进行+1,跳出阻塞,并返回_DSEMA4_TIMEOUT超时

    DISPATCH_TIME_FOREVER中即调用_dispatch_sema4_wait,表示会一直阻塞,知道等到single加1变为0为止,跳出阻塞


    3.dispatch_semaphore_signal

    了解了wait之后,对signal的理解也很简单。os_atomic_inc2o宏定义就是对dsema进行原子性+1的操作,如果大于0,则继续执行。

    总结一下信号的底层原理

    信号量在初始化时要指定 value,随后内部将这个 value 进行函数式保存。实际操作时会存两个 value,一个是当前的value,一个是记录初始 value。信号的 wait 和 signal 是互逆的两个操作,wait进行减1的操作,single进行加1的操作。初始 value 必须大于等于 0,如果为0或者小于0 并随后调用 wait 方法,线程将被阻塞直到别的线程调用了 signal 方法

    调度组 dispatch_group

    其实dispatch_group的相关函数的底层原理和信号量的底层原理的思想是一样的。都是在底层维护了一个value的值,进组和出组操作时,对value的值进行操作,达到0这个临界值的时候,进行后续的操作。

    1.dispatch_group_create

    和信号量类似,创建组后,对其进行了函数式保存dispatch_group_t,并通过os_atomic_store2o宏定义,内部维护了一个value的值

    2.dispatch_group_enter

    通过源码,我们可以知道进组操作,主要是先通过os_atomic_sub_orig2o宏定义,对bit进行了原子性减1的操作,然后又通过位运算& DISPATCH_GROUP_VALUE_MASK获得真正的value

    3.dispatch_group_leave

    出组的操作即通过os_atomic_add_orig2o的对值进行原子性的加操作,并通过& DISPATCH_GROUP_VALUE_MASK获取到真实的value值。如果新旧两个值相等,则执行_dispatch_group_wake操作,进行后续的操作。

    4.dispatch_group_async

    dispatch_group_async函数就是对enter和leave的封装。通过代码可以看出其和异步调用函数类似,都对block进行的封装保存。然后再内部执行的时候,手工调用了dispatch_group_enter和dispatch_group_leave方法。

    5.dispatch_group_notify

    通过源码,我们可以发现,通过调用os_atomic_rmw_loop2o在系统内核中获取到对应的状态,最终还是调用到了_dispatch_group_wake

    _dispatch_group_wake这个函数主要分为两部分,首先循环调用semaphore_signal告知唤醒当初等待 group 的信号量,因此dispatch_group_wait函数得以返回。


    总结

    dispatch_sync将任务block通过push到队列中,然后按照FIFO去执行。

    dispatch_sync造成死锁的主要原因是堵塞的tid和现在运行的tid为同一个

    dispatch_async会把任务包装并保存,之后就会开辟相应线程去执行已保存的任务。

    semaphore主要在底层维护一个value的值,使用signal进行+ +1,wait进行-1。如果value的值大于或者等于0,则取消阻塞,否则根据timeout参数进行超时判断

    dispatch_group底层也是维护了一个value的值,等待group完成实际上就是等待value恢复初始值。而notify的作用是将所有注册的回调组装成一个链表,在dispatch_async完成时判断value是不是恢复初始值,如果是则调用dispatch_async异步执行所有注册的回调。

    dispatch_once通过一个静态变量来标记block是否已被执行,同时使用加锁确保只有一个线程能执行,执行完block后会唤醒其他所有等待的线程。

    相关文章

      网友评论

          本文标题:GCD原理(下)

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