iOS 底层探索篇 —— GCD 源码分析

这是我参与8月更文挑战的第23天,活动详情查看:8月更文挑战

1. 死锁

所谓死锁,通常指有两个线程A和B都卡住了无法完成,都在等待对方完成任务后在执行。A不能完成是因为它在等待B完成。但B也不能完成,因为它在等待A完成。于是大家都无法完成任务,就导致了死锁(DeadLock)。

2 同步/异步函数

同步,异步区别

  1. 能否开辟新线程
  2. 任务回调是否具备异步性 - 同步性
  3. 死锁情况的产生

2.1 同步函数

下面分析串行和并行执行同步函数的情况。

串行

上文说到同步函数实现会走到这里,这里看到dq_width == 1 的时候,也就是当是串行队列的时候,会调用_dispatch_barrier_sync_f

在这里插入图片描述

搜索_dispatch_barrier_sync_f。发现调用了_dispatch_barrier_sync_f_inline

在这里插入图片描述

搜索_dispatch_barrier_sync_f_inline。

在这里插入图片描述

当死锁的时候,都会调用_dispatch_sync_f_slow,所以这里需要看_dispatch_sync_f_slow。这里也可以知道_dispatch_sync_f_slow不报错,报错的是 __DISPATCH_WAIT_FOR_QUEUE__

在这里插入图片描述

进去_dispatch_sync_f_slow。

在这里插入图片描述

接下来看报错的地方,也就是__DISPATCH_WAIT_FOR_QUEUE__,看到其报错的地方有条件判断,那么就进去看是如何判断是死锁的。这里的2个参数一个是线程状态,一个是线程id。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这里做了中间层封装,实际调用_dispatch_lock_is_locked_by

在这里插入图片描述

点进去_dispatch_lock_is_locked_by。DLOCK_OWNER_MASK是个很大的值,只要(lock_value ^ tid) 不为空,那么与上DLOCK_OWNER_MASK就不会为0。如果(lock_value ^ tid) & DLOCK_OWNER_MASK为0,那么就代表着(lock_value ^ tid)为0,那么就说明lock_value等于tid。那么就说明,本来是要调起这个队列,但是这个队列原定是要等待的,构成了一个矛盾,所以发生了死锁

在这里插入图片描述

在这里插入图片描述

并行

_dispatch_sync_f_inline里面还有这四个函数,并不知道并行队列是调用哪一个。这里注意到_dispatch_sync_invoke_and_complete的参数funcDISPATCH_TRACE_ARG( _dispatch_trace_item_sync_push_pop(dq, ctxt, func, dc_flags))之间没有逗号隔开,这是因为宏定义的时候,已经把逗号加上了

在这里插入图片描述

在这里插入图片描述

打下断点后运行,发现进入了_dispatch_sync_invoke_and_complete函数。

在这里插入图片描述

在这里插入图片描述

_dispatch_sync_invoke_and_complete进去后发现调用了3个函数,分别是:_dispatch_sync_function_invoke_inline_dispatch_trace_item_complete_dispatch_lane_non_barrier_complete

在这里插入图片描述

点击_dispatch_sync_function_invoke_inline后发现这里调用了_dispatch_client_callout,也就是block调用的地方。

在这里插入图片描述

2.2 异步函数

串行

上文说到,dispatch_async最终会调用dq_push,而dq_push在不同的线程下赋值是不一样的,这边来看一下串行队列时候的情况。
这里看到串行的dq_push调用的是_dispatch_lane_push

在这里插入图片描述

在这里插入图片描述

然后来到dx_wakeup也就是dq_wakeup,查一下定义是_dispatch_lane_wakeup

在这里插入图片描述

当要执行的时候,肯定是没有barrier的,所以状态为DISPATCH_WAKEUP_BARRIER_COMPLETE,会进入_dispatch_lane_barrier_complete

在这里插入图片描述

dq_items_tail不为空并且任务不是挂起的状态的时候才能执行,因为是串行,所以dq_width == 1,所以走_dispatch_lane_drain_barrier_waiter

在这里插入图片描述

然后走_dispatch_barrier_waiter_redirect_or_wake

在这里插入图片描述

这里会将dq的类型转为DISPATCH_QUEUE_GLOBAL_ROOT_TYPE类型,然后下面的dx_push就会调用_dispatch_root_queue_push。然后之后的步骤就和上文分析的差不多,最后调用_dispatch_client_callout调用block。

在这里插入图片描述

并行

并行的dq_push是_dispatch_lane_concurrent_push

在这里插入图片描述

然后搜索_dispatch_lane_concurrent_push

在这里插入图片描述

然后调用了_dispatch_continuation_redirect_push。这里会将dq的类型转为DISPATCH_QUEUE_GLOBAL_ROOT_TYPE类型,然后下面的dx_push就会调用_dispatch_root_queue_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_client_callout来调用block。那么这里的_dispatch_worker_thread2是哪里调用的呢?这里的_dispatch_worker_thread2封装给了pthread的api。GCD也是对pthread的封装。这里的调用执行,是通过workloop调用的,而不是立即执行,是受cpu进行调控处理的。

在这里插入图片描述

回到_dispatch_root_queue_poke_slow。如果是全局队列,那么就会创建线程去处理。

在这里插入图片描述

如果是普通的自己创建的线程,就会进行dowhile循环。这里dgq_thread_pool_size会暂时标记为1,这是因为正常的并行队列是0的,而全局队列为1的是因为他的线程量比正常的并行队列多一个

在这里插入图片描述

在这里插入图片描述

上文中说到并行队列的DQF_WIDTH DISPATCH_QUEUE_WIDTH_MAX

在这里插入图片描述

DISPATCH_QUEUE_WIDTH_POOL全局队列DQF_WIDTH,并且比DISPATCH_QUEUE_WIDTH_MAX大一

在这里插入图片描述

dgq_thread_pool_size会根据需求不断进行++

在这里插入图片描述

回到do while 循环,看到remaining = can_request,这里的can_request = t_count < floor ? 0 : t_count - floor;t_count是通过os_atomic_load2o得来的,而floor是之前传过来的参数

在这里插入图片描述

这里的remaining 是之前传过来的参数,值为1.

在这里插入图片描述

remaining 一般不会大于 can_request,否则就会报异常remaining需要的线程数,而can_request可以请求的线程数。这里如果大于,就会进行--的操作,如果remaining为0,那么就代表着线程池已经满了,那么就会直接return

3. 单例

单例是通过这样的方式来创建的。

在这里插入图片描述

源码中搜索dispatch_once,发现实际调用的是dispatch_once_f。这里的val是外面传进来的onceToken

在这里插入图片描述

这里对val进行了强制类型的转换。然后通过os_atomic_load获取标识符并赋值给V,如果标识符是DLOCK_ONCE_DONE,代表已经执行过,那么就直接return。如果 任务执行后,加锁失败了,则走到_dispatch_once_mark_done_if_quiesced函数,再次进行存储,将标识符置为DLOCK_ONCE_DONE
一般来说,第一次进来的话,会走到_dispatch_once_gate_tryenter这里,而里面做了解锁的操作,是对多线程的封装处理,所以是线程安全的。最后,就调用了_dispatch_once_callout。如果加锁了,那么就会调用_dispatch_once_wait进行无限制等待开锁的状态。

在这里插入图片描述

这里主要是通过底层os_atomic_cmpxchg方法进行对比,如果比较没有问题,则进行加锁,即任务的标识符置为DLOCK_ONCE_UNLOCKED

在这里插入图片描述

_dispatch_once_callout调用_dispatch_client_callout执行了block,调用_dispatch_once_gate_broadcast进行标记符的处理。

在这里插入图片描述

这里调用了_dispatch_once_mark_done进行标记符的处理。

在这里插入图片描述

这里就标记为DLOCK_ONCE_DONE了,这样下次进来的话,就会直接return了。

在这里插入图片描述

  • 单例只执行一次的原理:GCD单例中,有两个重要参数,onceTokenblock,其中onceToken是静态变量,具有唯一性,在底层被封装成了dispatch_once_gate_t类型的变量l,l主要是用来获取底层原子封装性的关联,即变量v,通过v来查询任务的状态,如果此时v等于DLOCK_ONCE_DONE,说明任务已经处理过一次了,直接return
  • block调用时机:如果此时任务没有执行过,则会在底层通过C++函数的比较,将任务进行加锁,即任务状态置为DLOCK_ONCE_UNLOCKED,目的是为了保证当前任务执行的唯一性,防止在其他地方有多次定义。加锁之后进行block回调函数的执行,执行完成后,将当前任务解锁,将当前的任务状态置为DLOCK_ONCE_DONE,在下次进来时,就不会在执行,会直接返回
  • 多线程影响:如果在当前任务执行期间,有其他任务进来,会进入无限次等待,原因是当前任务已经获取了锁,进行了加锁,其他任务是无法获取锁的
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享