在上篇文章函数与队列和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后会唤醒其他所有等待的线程。