这里不细聊 GCD 的 api 使用和概念, 不像 runtime, 这儿没有太多难以理解的概念.
这篇着重于, 我看完常用的数据结构体及接口函数后, 对 GCD 整体结构的理解:
- GCD 数据结构是啥
- GCD 的队列(main, global)是从哪来的, 自己创建的队列与他是啥关系
- 我们使用
async,sync时内部是怎样的流程 - 他又是怎么实现执行任务控制(
semaphore,group,barrier)的 - 其实最想的是想清楚这套框架的设计思路, 但目前只刚到了解原理, 还得花时间逐步深入
源码部分分布于各其他文章中, 里面主要是尝试读懂代码, 读完代码后的结论, 我放在本篇中:
- 其他: GCD 的
数据结构源码解析 - 其他: 源码中使用的宏
- 其他: GCD 中 isa 中有什么
- 其他: GCD 函数源码 —
GET与Create - 其他: GCD 函数源码 —
dispatch_sync&dispatch_async - 其他: GCD 函数源码 —
dispatch_once
参考文章: 逆水行舟
遗留问题:
- ❓dispatch_barrier_async 是怎么实现让后面的执行内容等待前面执行完的
- ❓底层是怎么唤醒让 GCD 的链表元素逐个执行的
- ❓创建线程在哪创的
-
基本数据结构
GCD 中常用的数据结构, 数据结构源码解析在此
里面具体探究了源码及内部宏的展开结构, 之后不再赘述.

上图中, 下层的宏字段由其上层的宏拼接出来, 需要知道的是, GCD 的一些常见数据类型都有以下特性:
- 都准备作为 链表节点 使用:
do_next(在_DISPATCH_OBJECT_HEADER宏中) - 都有决定在哪个 queue 执行的字段:
do_targetq(在_DISPATCH_OBJECT_HEADER宏中) - 都有内部和外部引用计数:
ref_cntxref_cnt(在_OS_OBJECT_HEADER宏中) - 都有一个类似于 runtim 中 isa 作用的, 虚函数表:
do_vtable(在_OS_OBJECT_HEADER宏中)- 虚函数表会在队列初始化时赋值
- 在整个流程中, 常用到的
.dq_push: 各种 async 本质是将任务 push 到队列中.dq_wakeup: 异步操作, 在尝试正在执行任务前, 都是通过 wakedo_dispose销毁函数,do_invoke触发执行函数
所有调用 GCD 传入的执行内容(block 或 函数)都会被包装成dispatch_continuation_s
(这个东西是独立的一套字段,和上面的宏没关系, 但是长的基本一样其他: GCD 的 数据结构 源码解析)
关于虚函数的使用
同步我们在后面会看到, 最简化 的流程是包装出dispatch_continuation_s, 然后直接当场执行
而异步复杂的多:
dispatch_async直接执行的只有dq_pushdq_push的主体是前面提到的dispatch_continuation_s, 由他执行的 push- 然后后续有一套唤醒流程, 调用的是
dq_wakeup
do_dispose主要在销毁时使用, 大部分的数据结构的 do_dispose 最终调的是 dealloc
关于主线程
需要注意的是, 主线程是特殊的! 主线程的数据结构是独立的dispatch_queue_static_s
比普通的 dispatch_queue_s, 内部主要多出一个链表 dq_items_tail, dq_items_head
并且在开始时已直接初始化好了
struct dispatch_queue_static_s _dispatch_main_q = {
DISPATCH_GLOBAL_OBJECT_HEADER(queue_main),
.do_targetq = _dispatch_get_default_queue(true),
.dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(1) |
DISPATCH_QUEUE_ROLE_BASE_ANON,
.dq_label = "com.apple.main-thread",
.dq_atomic_flags = DQF_THREAD_BOUND | DQF_WIDTH(1),
.dq_serialnum = 1,
};
关于线程控制数据结构
semaphore 和 group 的原理类似 (甚至看一些参考文章, 早期 group 内部是用 semaphore 实现的)
都是通过对一个标志位的 + - 操作, 来控制是否执行
group :
- 使用
dg_state/dg_bits/dg_gen作为执行控制, 这三玩意是个联合体.三个字段代表的其实是同一块内部地址, 使用中, 经常换字段通过来表达某种含义. group比semaphore多出来的是其内部存了notify的链表dg_notify_head,dg_notify_tail在合适的时机,这些 notify 会被逐个取出来执行
semaphore:
- 使用
dsema_value控制执行, 使用dsema_orig记录原始值
队列从哪来

系统默认提供的队列
前面提到, dispatch_main_queue 得天独厚, 从一开始就已经直接被赋值了, 这里不再赘述
我们看常用的 queue, dispatch_global_queue, 相关的源码解析我放在: 其他: GCD 函数源码 — GET 与 Create
get_queue 内部的核心部分叫 get_root_queue, 可以推测 GCD 底层有一个 Root Queue 的概念.
我们在源码中将 dispatch_get_global_queue 追到低, 最终会发现
get_global_queue 本质上是从已经生成的数组中,找一个与入参相符的元素
return &_dispatch_root_queues[2 * (qos - 1) + overcommit];
在_dispatch_root_queues[] 这个变量中, 定义了序号 4~15 共 12 初始队列, 这些队列名字在调试时会经常看到
”com.apple.root.maintenance-qos"”com.apple.root.maintenance-qos.overcommit""com.apple.root.background-qos"”com.apple.root.background-qos.overcommit""com.apple.root.utility-qos""com.apple.root.utility-qos.overcommit""com.apple.root.default-qos""com.apple.root.default-qos.overcommit""com.apple.root.user-initiated-qos""com.apple.root.user-initiated-qos.overcommit""com.apple.root.user-interactive-qos""com.apple.root.user-interactive-qos.overcommit"
我们还注意到, main_queue 是 1, 这里是 4~15, 那么 2,3 哪去了?
被放在两个 mgr中 _dispatch_mgr_q, _dispatch_mgr_root_queue (我还没研究透, 先不深入)
overcommit 据一些介绍文章说, 是会独立开辟线程. 这点也是暂时源码还没看透, 先不深入
我们创建的队列
GCD 有提供dispatch_queue_create & dispatch_queue_create_with_target 接口供我们创建新的队列
源码解析写在 其他: GCD 函数源码 — GET 与 Create
从源码中可以得知, 这两函数内部都是在调用同一个函数 _dispatch_lane_create_with_target
区别在于不同的入参会导致最后 target 有所不同, 这个 target, (上面数据结构中, 能看到基本数据结构都带着 do_targetq), 将决定行为在哪发生
(入参中的 dispatch_queue_attr_s 在源码中是只有 OS_OBJECT_STRUCT_HEADER 宏的结构体, 也就是其中只包含一些基本的字段,可以理解只是 GCD 中最简单的基类)
省略掉中间的一些列过程, 会发现:
- 如果没指定 Target, 那么新建的 Queue, 其 Target 会变成 Root Queue 的其中一个
- 如果指定了 Target, 那么新建的 Queue, 其 Target 会直接或间接的变成 Root Queue 的其中一个
(因为没有 target 不是root queue的新 queue, 无论怎么引用, 最终都会跑到root queue身上)
也就是说 我们创建的 queue 中的任务执行时其实是被初始定义的 root_queue 所控制的
队列是怎么用的
源码分析在这: 其他: GCD 函数源码 — dispatch_sync &dispatch_async
sync
这个 sync 不单指dispatch_sync而是 GCD api 的所有 sync 操作,
源码中函数跳转很多很多, 关注里面提到的执行流程:
- 串行的情况下, 主流程共 8 步, 中间涉及函数主要是
barrier类型, 最终调用函数为_dispatch_client_callout - 并发的情况下, 主流程共 6 步, 中间涉及的函数直接就叫
sync不带battier 前缀, 最终函数_dispatch_client_callout
可以看到 不论并发还是串行, 最终的执行都是一样的, _dispatch_client_callout 是直接对入参函数的调用
在这个过程中, 一旦出现无法直接执行的条件, 最终都会跳入_dispatch_sync_f_slow, 根据上下文选择等待执行的策略
可以理解为 sync 就是根据状态判断是否能直接执行任务, 能则直接执行, 不能则选择一种等待策略
async
异步的操作我们要明确一件事, 不论 dispatch_async, dispatch_group_async, dispatch_barrier_async 等.他们直接的操作,都只是将任务封装成 _dispatch_continuation_t 并设置对应的 flag, 然后执行_dispatch_continuation_async
async 实质的直接操作只有 push: 将任务推到某个队列中
(后面我们讲的到 async, 最终都是不同 flag 的 push)
push 到队列后, 之后的调度到了系统底层, 这一步开始已经脱离了 async 流程
由系统底层控制, 这里开始我追溯起来太吃力. 只能根据参考文章, 找到底层调用的上层的函数_dispatch_worker_thread2 开始执行流程.
这套执行流程内部会 逐个将链表元素取出, 然后执行 do_invoke
所以总结来看, async 的任务其实分两步, 第一步是将执行体扔到链表中; 第二部是底层触发的调用, 逐个遍历了链表取出,然后执行
ps. 很遗憾的是, 我最终没弄明白被 push 的任务最后的触发执行流程以及对 Flag 的控制, GCD 有个很重要的点就在函数触发前如何控制这里.
通过参考文章, 后续流程已经到XNU. 而我在找函数时, 底层大多都是汇编实现, 找不到主动将 push 的内容拿出来执行的地方(能找到最终执行函数,往上找调用者,分支太多了,不知道哪一分支是)
执行控制
常用的执行控制有四个
dispatch_once: 执行一次dispatch_barrier: 前面执行完才能执行后面dispatch_group: 组内执行完统一处理dispatch_semaphore: 卡主线程,直到允许放行
dispatch_once
dispatch_once 重点在于怎么在多线程环境下只执行一次,
这个实现挺好想到的, 执行前用一个标志位做判断, 执行后将标志位改回来. 这两个操作放在同一个锁或同一个原子操作中
(分开处理就可能出现, 一个开始判断, 另一个线程准备改值,然后同时进入执行函数的情况)
源码解析在这: 其他: GCD 函数源码 — dispatch_once
苹果实现是 对比较和修改值作为原子操作, 所以避开了比较后另一个线程同时进入比较的情况
核心代码是下面的函数, 入参1 与入参 2 比较, 相等的情况, 入参 1 被改为入参 3
os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED, (uintptr_t)_dispatch_lock_value_for_self(), relaxed);
这比较有意思的是 _dispatch_lock_value_for_self, 内部取了一个特定的线程叫 _PTHREAD_TSD_SLOT_MACH_THREAD_SELF
然后用这个线程的 tid & 一个值 来作为锁标记
semaphore & group
这两用途不同, 内部实现的原理是类似的, 都通过一个标志位, 判断当前是否能执行 ”后续的工作”
类似的控制逻辑
初始化: 初始化没太多骚操作, 都是 alloc 对应的结构体对象然后赋予初始值
标志位: 参考对数据结构的解析 其他: GCD 数据结构 源码解析:
semaphore通过数据结构中的dsema_value控制,group通过数据结构中下面这个联合体控制, 这里三个字段,在源码中会根据需要出现, 实际上, 都是代表同一地址下的数据DISPATCH_UNION_LE(uint64_t volatile dg_state, uint32_t dg_bits, uint32_t dg_gen ) DISPATCH_ATOMIC64_ALIGN;
semaphore 的 signal wait, group 的 enter, leave 函数内部简化后的操作, 都有在对这个标志位做原子加减.
做的事情: 参考对函数的解析其他: GCD 函数源码 — semaphore & group & barrier:
semaphore_wait: 一旦-1后发现标志位小于 0, 就不止执行 return, 而是走到 do-while, 卡死线程
(执行函数在_dispatch_sema4_timedwait或_dispatch_sema4_wait, 内部都是 do-while)group_leave: 一旦-1后发现标志位等于 0, 就会触发 wake, 内部实质是遍历他的 notify 链表,逐个执行
(执行力函数在_dispatch_group_wake, 内部是 do-while 遍历 group 存的链表)
其他函数:
dispatch_group_notify: 前文中提过 group 里有 notify 的头节点和尾节点, 这个函数简化后,就是在往链表中加数据
dispatch_group_async: 实质是两个函数的集成 1.dispatch_group_enter , 2._dispatch_continuation_async
- 其内部最终的步骤是
_dispatch_continuation_async(我们会发现 async 操作,最终都是这个) - 与其他 async 不同在与, flag 为
DC_FLAG_CONSUME | DC_FLAG_GROUP_ASYNC - 没有直接的 leave, leave 在任务真正执行时调用的
_dispatch_continuation_with_group_invoke中
不同的行为
对 semaphore 来说, 他控制执行的方式是堵住当前线程, 直到外界条件允许.
而 group, 不会干影响当前线程的事, 它主要是对加进自己组中的内容做管理, 并在组内任务都完成时统一执行一些操作
semaphore 有一点特殊的是, 他会在销毁时检查 dsema_value, 如果和 dsema_orig 不一致, 就会引发 crash
if (dsema->dsema_value < dsema->dsema_orig) {
DISPATCH_CLIENT_CRASH(dsema->dsema_orig - dsema->dsema_value,
"Semaphore object deallocated while in use");
}
dispatch_barrier_async
函数源码解析在这 其他: GCD 函数源码 — semaphore & group & barrier
barrier_async 操作其实没太多可看的, 基本上所有的 async 操作, 都是调用当前结构体, 函数表中的 dx_push
也就是说 async 操作, 本质是把一个任务 push 到队列中, 然后再视情况要不要下一步
大部分的 push, 是由任务本身(block 或函数) 封装出来的结构体_dispatch_continuation_t 执行的 push
barrier_async 的特殊之处在于, 其 push 的入参 flag 为 DC_FLAG_CONSUME | DC_FLAG_BARRIER
barrier并没有直接控制执行过程, 而是通过任务的参数, 在执行过程中间接的影响过程
需要注意的, barrier 的队列必须是自己创建的队列
在整个 GCD 调用流程中,有很多代码在判断_dispatch_queue_try_acquire_barrier_sync, 我用DC_FLAG_BARRIER追溯, 怀疑是这个判断, 让处在栅栏后的内容没有执行