一、前言
大部分的GCD操作都离不开队列(queue):使用dispatch_get_main_queue
获取主队列,使用dispatch_queue_create
创建一个自定义的队列,使用dispatch_get_global_queue
获取一个全局的并发队列等等。那么GCD是如何通过这些队列实现多线程的呢?它又是如何管理这些队列的呢?dispatch_async/dispatch_sync
是如何工作的呢?带着这些问题,我们从源码中寻找答案。
二、队列与线程
首先,借用一张网上的图片直观地描述GCD队列和线程的关系。
通过GCD,我们可以很方便地实现多线程,而不需要过多地关注线程的实现和创建等,GCD内部维护了一个线程池,由系统根据任务的数量和优先级动态地创建和分配线程执行。
我们提交的任务由GCD内部的manager queue管理一层层地分发到target queue中,最终汇聚到root queue中并由线程池管理线程来执行任务。
线程和队列并不是一对一的关系,一个线程中可能有多个串行或并行队列,这些队列按照同步或异步的方式工作。
注:为方便阅读,文中的所有代码均为宏展开后的代码。
GCD中队列的种类
从libdispatch源码中可以看到,GCD中一共有如下几种队列:
- 主队列,使用
dispatch_get_main_queue()
获得的队列,与主线程绑定 - 全局队列,使用
dispatch_get_global_queue()
获得的队列,是并行队列,由GCD创建并管理,也是libdispatch内部使用的root-queue - 自定义队列,使用
dispatch_queue_create()
创建的队列,为串行或并行队列 - 管理队列,libdispatch内部使用的队列,不暴露给开发者,作为队列的调度管理者使用
- Runloop队列,用于与线程绑定的
dispatch_queue
,比如:提交到main-queue上的任务是由runloop-queue进行管理并最终调度到main thread的runloop中处理。
主队列main queue
在dispatch_get_main_queue()
的函数声明处有如下内容:
1 | /*! |
可以看出主队列在main()
函数调用之前被创建,同时也说明了libdispatch库的初始化工作在main函数之前就完成了。
调用dispatch_get_main_queue()
返回的是_dispatch_main_q
这样一个dispatch_queue_t
类型的结构体。
1 | dispatch_queue_t dispatch_get_main_queue(void) |
_dispatch_main_q
的结构:
1 | struct dispatch_queue_s _dispatch_main_q = { |
从代码中可以看到_dispatch_main_q
包含以下属性:
1、.do_vtable、._objc_isa
- do_vtable 包含了队列的类型、dispose、invoke、push、wakeup和debug等信息,这些信息与队列和任务的调度有关
1 | DISPATCH_VTABLE_SUBCLASS_INSTANCE(queue_main, queue, |
- _objc_isa main_queue的isa指针指向OS_dispatch_queue_main_class
2、.do_ref_cnt、.do_xref_cnt
队列的内部、外部引用计数都赋值为DISPATCH_OBJECT_GLOBAL_REFCNT
,而DISPATCH_OBJECT_GLOBAL_REFCNT
的实际定义为INT_MAX
,说明了主队列的生命周期与App的生命周期一致,开发者无需对主队列进行retain/release操作,其生命周期由GCD管理。
1 |
3、.do_targetq
主队列的target queue为&_dispatch_root_queues[
DISPATCH_ROOT_QUEUE_IDX_DEFAULT_QOS_OVERCOMMIT],
,实际上就是serialnum=11的”com.apple.root.default-qos.overcommit”这个全局队列,注意这里虽然有个条件编译命令:#if !DISPATCH_USE_RESOLVERS
,但是实际上在libdispatch_init()
函数中又一次设置了main_queue的do_targetq:
1 |
|
所以无论条件编译是否命中,主队列的target queue都被设置为”com.apple.root.default-qos.overcommit”这个root queue。
GCD中所有的非全局队列(自定义队列及内部的管理队列)的任务最终都是要提交到全局队列(即:root queue)中处理,主队列除外,主队列与主线程绑定,提交到主队列中的任务由runloop queue管理并提交到主线程的runloop执行。
全局队列global queue
GCD内部维护12个全局队列,对应上述的四个优先级:High/Default/Low/Background。
1 | struct dispatch_queue_s _dispatch_root_queues[] = { |
dq_serialnum为队列号,全局队列的队列号从4开始,前面三个分别为:
- 主队列,dq_serialnum = 1
- 管理队列(_dispatch_mgr_q),dq_serialnum = 2
- dispatch_mgr_root_queue(_dispatch_mgr_q的目标队列),dq_serialnum = 3
管理队列manager queue
1 | struct dispatch_queue_s _dispatch_mgr_q = { |
从代码可以看出,manager queue是作为root queue与线程池之间的调度和管理者的,如:GCD Timer的实现就用到了管理队列
自定义队列
使用dispatch_queue_create
创建自定义队列,为方便阅读,只保留主要流程。
1 | static dispatch_queue_t |
根据传入的参数调用_dispatch_queue_init
创建相应的串行或并行队列,然后设置label、队列优先级,并设置target queue为dispatch_root_queue
1 | tq = _dispatch_get_root_queue( |
targetq的作用就是将push到queue中的任务(可能是任务也可能是queue)层层向上push,最终push到全局队列中,由全局队列调度线程池来执行任务(或者pop queue)。
While custom queues are a powerful abstraction, all blocks you schedule on them will ultimately trickle down to one of the system’s global queues and its thread pool(s).
虽然自定义队列是一个强大的抽象,但你在队列上安排的所有Block最终都会渗透到系统的某一个全局队列及其线程池。
Runloop队列
Runloop队列用于与线程绑定的队列的任务调度,比如主队列,看下main queue的wake up函数:
1 | void |
_dispatch_queue_is_thread_bound
函数判断当前队列是否是与线程绑定的,由于主队列是绑定在主线程上的,这里就调用了_dispatch_runloop_queue_wakeup
函数,由runloop queue将任务调度到主线程的runloop上,最终由主线程runloop在合适的时机执行。
GCD dispatch流程分析
上一篇文章说过,dispatch_queue_s
结构体中有do_vtable
元素,这个do_vtable
中包含了队列的push/wakeup/invoke/dispose等与dispatch相关的信息:
1 | DISPATCH_VTABLE_SUBCLASS_INSTANCE(queue_main, queue, |
其它队列,如:manager queue、runloop queue、root queue等也有类似的vtable结构:
1 | /// manager queue vtable |
- .do_push: 调用push操作将任务提交到queue上
1 | static void _dispatch_continuation_push(dispatch_queue_t dq, dispatch_continuation_t dc) |
- .do_wakeup: 当push任务到队列中时,会调用do_wakeup唤醒队列
1 | static inline void |
- .do_invoke: 在libdispatch中,do_invoke只在以下3种情况执行:1、任务出队;2、runloop queue出队;3、如果queue重写了invoke,则当queue元素出队时,调用
_dispatch_queue_override_invoke
,在_dispatch_queue_override_invoke
函数中调用do_invoke。
1 | /// 1. |
除了主队列,其他所有队列中提交的任务最终都要通过层层的target queue提交到root queue中(把queue整体提交到target queue中),从线程池中取出或新建一个线程执行:
1 | void |
那么队列的dispatch的大致流程如下所示:
提交任务到queue -> 调用push将任务入队 -> 调用wakeup唤醒队列 -> 如果有target queue,往上提交,直到root queue为止 -> 由root queue从线程池中取出或创建一个新线程执行任务。
主队列的dispatch流程与上述略有不同,提交到主队列的任务由GCD内部的runloop queue管理并最终由主线程的runloop执行。
dispatch_async分析
先看下主队列上的异步任务。
主队列的dispatch_async
如果我们想在主线程中执行一个异步操作,通常的做法:
1 | dispatch_async(dispatch_get_main_queue(), ^{ |
上述这段代码在libdispatch内部的流程如下:
1、将传入的block任务转换成一个dispatch_continuation_t
类型的结构体对象,然后调用_dispatch_continuation_async
将continuation push到main queue中。
1 | void dispatch_async(dispatch_queue_t dq, dispatch_block_t work) |
注:在libdispatch源码中能经常见到fastpath、slowpath、likely、unlikely等宏,编写这些宏的目的是告诉编译器来对我们的代码进行优化,通常:
- fastpath/likely 表示条件更可能成立
- slowpath/unlikely 表示条件更不可能成立
2、唤醒main queue
1 | _dispatch_continuation_push(dispatch_queue_t dq, dispatch_continuation_t dc) |
3、wakeup runloop queue
1 | void _dispatch_main_queue_wakeup(dispatch_queue_t dq, dispatch_qos_t qos, |
4、唤醒主线程,并注册回调函数,由mach内核在合适的时机执行_dispatch_main_queue_drain
操作
1 | static inline void _dispatch_runloop_queue_class_poke(dispatch_queue_t dq) |
5、执行_dispatch_continuation_pop_inline
函数,如果主队列中有未完成的任务,将任务出队并执行。
1 | static void _dispatch_main_queue_drain(void) |
至此,主队列的dispatch_async流程执行完毕,其实我们在xcode断点时的堆栈信息也能窥探一二。
自定义队列/全局队列上的dispatch_async
示例代码
1 | // 1、自定义串行队列 |
自定义创建的并行队列比其他队列(串行队列和全局队列)多了一个redirection流程
1 | static inline void _dispatch_continuation_async2(dispatch_queue_t dq, dispatch_continuation_t dc, |
代码中DISPATCH_QUEUE_USES_REDIRECTION
这个宏是用来判断queue是否需要redirection,如果dq_width
满足width > 1 && width < 0xfff
条件,则队列需要热direction。串行队列(dq_width
= 1)和全局队列(dq_width
= 0xfff)都不满足上述条件,无需direction。
redirection:
1 | static void |
redirection操作的目的就是要将任务最终push到root queue中。
对于无需redirection的队列,调用其push函数,将任务push到队列中,分两种情况:
- 普通的自定义队列:如果queue有target_queue,调用
_dispatch_queue_push_queue
,将queue层层向上push到target_queue中,最终push到root queue中。 - 全局队列:由于全局队列(root queue)没有target_queue,调用
_dispatch_root_queue_push
直接把任务push到root queue中。
最终,所有提交到非主队列的任务都push到了root queue中,由root queue调度线程池并分配线程执行。
1 | /// root queue线程池管理相关 |
那么root queue是如何出队的呢?上述代码的do-while循环中调用了pthread_create
创建新的线程,并将线程运行函数起始地址指向_dispatch_worker_thread
,那么线程创建后会执行_dispatch_worker_thread
。
1 | static void *_dispatch_worker_thread(void *context) |
在_dispatch_worker_thread
中进行drain root queue,将root queue中的元素一个个出队,元素出队时调用_dispatch_continuation_pop_inline
,触发队元素的.do_invoke
,执行任务。
总结
整理一下dispatch_async的流程:
dispatch_sync分析
dispatch_sync的流程与上文分析的大同小异,一般来说同步任务是在当前线程中执行,同时它会阻塞当前线程直到任务执行完毕。
- 当queue时串行队列时,当前线程会获取lock,如果成功则执行任务,否则出发crash,比如
- 当queue是并行队列时,会直接执行任务。
关于dispatch_sync的流程不详细分析了,这里重点关注一下lock机制以及引起死锁的情形。
dispatch_sync_f
函数:
1 | void |
获取队列的lock
dispatch_sync_f
函数中,如果是串行队列,执行dispatch_barrier_sync_f
,一步步往下执行,会看到lock相关的函数:_dispatch_queue_try_acquire_barrier_sync_and_suspend
。
使用宏替换后的代码:
1 | static inline bool |
从上述代码可以看出,libdispatch通过一些原子操作来比较queue.dq_state的值来实现lock操作:
1、如果dq_state为初始值(init | role
),说明当前queue没有被任何线程lock,则lock成功并设置dq_state为(value | role
);
2、否则lock失败,返回false。
线程获取到queue的lock后,queue.dq_state中同时也记录了当前持有lock的线程的tid信息。
再返回到上一级函数dispatch_barrier_sync_f
中
1 | void dispatch_barrier_sync_f(dispatch_queue_t dq, void *ctxt, |
当lock失败后,进入到_dispatch_sync_f_slow
等待上一个任务执行完成:
1 | static void |
死锁分析
死锁的产生必须满足四个必要条件:
- 资源互斥访问
- 请求与保持
- 不剥夺
- 循环等待
libdispatch中创建并分配线程来执行任务块的过程中,线程/队列对资源的操作满足上述前三个条件,那么如果再满足第四个条件,必然会发生死锁。
在GCD中满足两个条件即会形成循环等待的情形:
- 串行队列正在执行任务Task 1(无论是sync还是async方式提交的)
- Task 1未执行完成,又向队列中同步提交Task 2(dispatch_sync方式提交)
死锁的示例代码:
1 | dispatch_queue_t serialQ = dispatch_queue_create("com.testqueue.serial", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL); |
新版libdispatch中引入了死锁检测机制,发生死锁时,主动触发程序crash,并定位到引起死锁的代码,降低了调试难度。
死锁检测相关的代码:
1 | dq_state = _dispatch_sync_wait_prepare(dq); |
从中可以看到,libdispatch是通过(dq_state ^ tid) & DLOCK_OWNER_MASK
这句代码判断dq是否被某个线程持有,而dq.dq_state中存有持有它的线程的tid信息,如果对应tid的线程持有了dq,则返回true,说明当前线程已持有dq,循环等待条件成立,产生死锁,否则返回false。
那么问题来了,如果在thread_A中提交Task1,在Task1还在执行时,在thread_B中同步提交Task2会发生什么情况呢,GCD能否检测出死锁呢?
测试代码中为了避开主线程死锁对测试的干扰,采用dispatch_async方式提交Task1。
1 | - (void)viewDidLoad { |
输出如下:
1 | Test[73008:2443797] main queue complete. |
此时程序卡住并没有crash,xcode也并未提示任何crash信息。
这说明了libdispatch死锁检测机制的问题:它只针对在同一个线程中向串行队列同步提交任务的情况。如果Task 2是在其它线程中同步提交的,它就无法检测出来了。
三、总结
本文只是粗略地分析了GCD的queue、dispatch过程以及queue和线程之间的调度关系,libdispatch源码的繁杂远远不止于此,细节之处还有如:mach_port通信、线程tsd、runloop callback、系统内核(libkern)交互等等,同时还有一些提升程序性能的编程技巧等(libdispatch为了最大限度地提升性能,大量使用了原子操作而非pthread_mutex_lock
、OSSpinLock/OSUnfairLock
等锁来实现同步)。感兴趣的同学可以把源码下载下来仔细阅读一下。