GCD
前言
在日常开发中,GCD是我们常用的多线程开发工具,它隔离了我们与线程间的直接交互,所以在使用过程中我们很少关注到GCD的底层原理。此次带着几个问题来对GCD源码进行分析。源码版本339.92.1。
dispatch_async
如何实现的,分发到主队与全局队列有什么区别,一定会新建线程执行任务么?dispatch_sync
如何实现的,为什么说 GCD 死锁是队列导致的而不是线程,死锁不是操作系统的概念么?- 信号量是如何实现的,有哪些使用场景?
dispatch_group
的等待与通知。dispatch_once
如何实现?dispatch_source
用来做定时器如何实现,有什么优点和用途?dispatch_suspend
和dispatch_resume
如何实现,队列的的暂停和计时器的暂停有区别么?
Dispatch 源码分析
Dispatch 中常用的宏定义及基础知识
DISPATCH_DECL
#define DISPATCH_DECL(name) typedef struct name##_s *name##_t
GCD中的变量大多使用了这个宏,比如DISPATCH_DECL(dispatch_queue)展开后是
typedef struct dispatch_queue_s *dispatch_queue_t;
它的意思是定义一个dispatch_queue_t类型的指针,指向了一个dispatch_queue_s类型的结构体。
fastpath vs slowpath
1 | #define fastpath(x) ((typeof(x))__builtin_expect((long)(x), ~0l)) |
__builtin_expect
是编译器优化汇编代码的,fastpath(x) 依然返回 x,只是告诉编译器 x 的值一般不为 0,从而编译器可以进行优化。同理,slowpath(x) 表示 x 的值很可能为 0,希望编译器进行优化。
TSD
Thread Specific Data(TSD)是指线程私有数据。在多线程中,会用全局变量来实现多个函数间的数据共享,局部变量来实现内部的单独访问。TSD则是能够在同一个线程的不同函数中被访问,在不同线程时,相同的键值获取的数据随线程不同而不同。可以通过pthread的相关api来实现TSD:1
2
3
4
5
6//创建key
int pthread_key_create(pthread_key_t *, void (* _Nullable)(void *));
//get方法
void* _Nullable pthread_getspecific(pthread_key_t);
//set方法
int pthread_setspecific(pthread_key_t , const void * _Nullable);
原子操作
c++原子操作库1
2
3
4
5
6
7
8
__sync_lock_test_and_set((p), (n))
将p设为value并返回p操作之前的值。__sync_bool_compare_and_swap((p), (o), (n))
这两个函数提供原子的比较和交换:如果p == o,就将n写入p(p代表地址,o代表oldValue,n代表newValue)__sync_add_and_fetch((p), 1)
先自加1,再返回__sync_sub_and_fetch((p), 1)
先自减1,再返回__sync_add_and_fetch((p), (v))
先自加v,再返回__sync_sub_and_fetch((p), (v))
先自减v,再返回__sync_fetch_and_or((p), (v))
先返回,再进行或运算__sync_fetch_and_and((p), (v))
先返回,再进行与运算libdispatch 关键数据结构
源码中数据结构的命名一般是以_s和_t结尾,其中_t是_s的指针类型,_s是结构体。比如dispatch_queue_t和dispatch_queue_s。
dispatch_object_s
dispatch_object_s
是GCD最基础的结构体,相当于基类,类似OC中的id类型。定义如下
1 | struct dispatch_object_s { |
dispatch_object_t
dispatch_object_t是个union的联合体,可以用dispatch_object_t代表这个联合体里的所有数据结构。
union与结构体有本质的不同,结构体中各成员有各自的内存空间,联合体中的各成员共享一段内存空间,一个联合变量的长度等于各成员最长的长度。用于节省内存。
1 | typedef union { |
dispatch_xxx_vtable
DISPATCH_VTABLE_HEADER(x)
vtable结构体定义,包含了这个dispatch
_object_s 的操作函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned long do_type; \ //dispatch_object_s类型
const char *do_kind; \ //do 的说明
size_t (*do_debug)(struct dispatch_#
void (*do_invoke)(struct dispatch_#
unsigned long (*do_probe)(struct dispatch_#
void (*do_dispose)(struct dispatch_#
//dx_xxx 开头的宏定义,本质是调用vtable中定义的函数
dispatch_continuation_s
dispatch_continuation_s
结构体主要封装block和function,被传入的block会变成这个结构体对象传入队列。定义如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
_OS_OBJECT_HEADER( \
const void *do_vtable, \
do_ref_cnt, \
do_xref_cnt); \ //os_object_header
struct dispatch_#
dispatch_function_t dc_func; \ // 如 _dispatch_call_block_and_release 方法,结构体实际执行的方法
void *dc_ctxt; \ // 调用dispatch_async 传入的block 即待执行的内容,会作为参数传入dc_func
void *dc_data; \ //相关数据
void *dc_other; //其他
struct dispatch_continuation_s {
DISPATCH_CONTINUATION_HEADER(continuation);
};
dispatch_queue_s
dispatch_queue_s
是队列的结构体,是我们接触最多的结构体。
1 | struct dispatch_queue_s { |
dispatch_queue_create 实现
dispatch_queue_create
用来创建自定义队列,流程图和源码如下:
1 | // skip zero |
自定义队列创建流程:
- 申请内存空间,设置基本属性,默认并发数
do_width=1
。 - 根据
attr
的属性值(nil
、DISPATCH_QUEUE_SERIAL
(实际上就是 nil) 或DISPATCH_QUEUE_CONCURRENT
)设置目标队列,如果为Concurrent 设置并发数UINT32_MAX
。 _dispatch_get_root_queue
会获取一个全局队列,它有两个参数,分别表示优先级和是否支持 overcommit。一共有四个优先级,LOW、DEFAULT、HIGH 和 BACKGROUND,因此共有 8 个全局队列。带有 overcommit 的队列表示每当有任务提交时,系统都会新开一个线程处理,这样就不会造成某个线程过载(overcommit)。- 设置
dq->do_targetq = tq;
,向队列提交的任务,会被放到它的目标队列来执行。普通串行队列的目标队列就是一个支持overcommit
的全局队列,全局队列的底层是一个线程池。
dispatch_async 实现
直接调用dispatch_async_f
方法。
1 | void |
- 如果是串行队列(dq_width = 1)调用
dispatch_barrier_async_f
函数处理 - 如果有
do_targetq
目标队列,则进行转发 - 否则调用
_dispatch_queue_push
将封装好的dc放入队列中。
将_dispatch_queue_push
宏展开,调用栈如下:1
2
3
4
5
6
7_dispatch_queue_push(dq,dc)
└──_dispatch_trace_queue_push(dq, _tail)
└──_dispatch_queue_push(dq,_tail) {struct dispatch_object_s *tail = _tail._do;}
//判断链表中已经存在节点,将tail(即dc)放在链表尾部
└──if(!dispatch_queue_push_lists2(dq, tail, tail))
//否则将任务放在链表头部
└── _dispatch_queue_push_slow(dq, tail);
1 | static inline void |
由上面的代码可以看出_dispatch_queue_push分为两种情况:
1、如果队列的链表不为空,将节点添加到链表尾部,即dq->dq_item_tail=dc。然后队列会按先进先出(FIFO)来处理任务。
2、如果队列此时为空,进入到_dispatch_queue_push_slow函数。如果队列是全局队列会进入if分支,原子性的将节点添加到队列开头,并执行_dispatch_queue_wakeup_global唤醒全局队列;如果队列是主队列或自定义串行队列if分支判断不成立,执行_dispatch_queue_push_list_slow2函数,它会将节点添加到队列开头并执行_dispatch_wakeup函数唤醒队列。
dispatch_async第一阶段的工作主要是封装外部任务并添加到队列的链表中,可以用下图来表示:
接着来看队列唤醒的逻辑,主要分成主队列和全局队列的唤醒和任务执行逻辑:
- 如果是主队列,会先调用_dispatch_wakeup唤醒队列,然后执行_dispatch_main_queue_wakeup函数来唤醒主线程的Runloop,代码如下:
1 | dispatch_queue_t _dispatch_wakeup(dispatch_object_t dou) { |
当我们调用 dispatch_async(dispatch_get_main_queue(), block)
时,libDispatch
向主线程的 RunLoop
发送消息,RunLoop
会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
里执行这个 block。
- 如果是全局队列,调用
_dispatch_queue_wakeup_global
函数,它封装调用了核心函数_dispatch_queue_wakeup_global_slow
,调用栈和核心代码如下:
1 | _dispatch_queue_wakeup_global_slow(dq) |
1 | static void _dispatch_queue_wakeup_global_slow(dispatch_queue_t dq, unsigned int n) { |
详细代码说明在这里,检测当前线程池是否可用(已满),未满创建新的线程。创建新的线程后执行_dispatch_worker_thread
函数。
1 | _dispatch_worker_thread |
1 | static void * _dispatch_worker_thread(void *context) { |
1 | static inline void _dispatch_continuation_pop(dispatch_object_t dou) { |
总结:dispatch_async
的流程是用链表保存所有提交的block,然后在底层线程池中,依次取出block并执行;而向主队列提交block则会向主线程的Runloop发送消息并唤醒Runloop,接着会在回调函数中取出block并执行。
dispatch_sync
dispatch_sync
主要封装了 dispatch_sync_f
函数,具体实现如下:
1 | void |
从上面代码可以看出,后续主要分为两种情况:
- 向串行队列提交同步任务,执行
dispatch_barrier_sync_f
函数:
1 | void |
如果队列无任务执行,调用_dispatch_barrier_sync_f_invoke
执行任务。
1 | static void _dispatch_barrier_sync_f_invoke(dispatch_queue_t dq, void *ctxt, |
如果队列存在其他任务或者被挂起,调用_dispatch_barrier_sync_f_slow
函数,等待该队列的任务执行完成后用信号量通知队列继续执行任务。向当前串行队列提交任务就会走到如下分支,导致死锁。
1 | static void _dispatch_barrier_sync_f_slow(dispatch_queue_t dq, void *ctxt, dispatch_function_t func) |
- 向并发队列提交同步任务,执行
_dispatch_sync_f2
函数。如果thread存在其他任务,或者队列被挂起,或者有正在执行的任务,则调用_dispatch_sync_f_slow函数,使用信号量等待,否则直接调用_dispatch_sync_f_invoke执行任务。
1 | static inline void |
dispatch_barrier_async
dispatch_barrier_async
是OC中解决线程同步的一种方法
它调用了dispatch_barrier_async_f
函数,与dispatch_async
类似但是do_vtable多了一个标志位DISPATCH_OBJ_BARRIER_BIT
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17void
dispatch_barrier_async_f(dispatch_queue_t dq, void *ctxt,
dispatch_function_t func)
{
dispatch_continuation_t dc;
dc = fastpath(_dispatch_continuation_alloc_cacheonly());
if (!dc) {
return _dispatch_barrier_async_f_slow(dq, ctxt, func);
}
// 区别于dispatch_async 多了个标志位 DISPATCH_OBJ_BARRIER_BIT,从队列中取任务时会用到
dc->do_vtable = (void *)(DISPATCH_OBJ_ASYNC_BIT | DISPATCH_OBJ_BARRIER_BIT);
dc->dc_func = func;
dc->dc_ctxt = ctxt;
_dispatch_queue_push(dq, dc);
}
dispatch_barrier_async
如果传入的是global queue
,在唤醒队列时会执行_dispatch_queue_wakeup_global
函数,故执行效果同dispatch_async
一致,栅栏方法会无效;- 如果传入的是自定义队列,
_dispatch_continuation_pop
参数是自定义的queue,在取出任务时会用到DISPATCH_OBJ_BARRIER_BIT
标记,调用栈如下:
1 | _dispatch_queue_invoke |
1 | _dispatch_thread_semaphore_t _dispatch_queue_drain(dispatch_object_t dou) { |
在while循环中依次取出任务并调用_dispatch_continuation_redirect
函数,使block并发执行。当遇到DISPATCH_OBJ_BARRIER_BIT
标记时,直接goto out,返回一个空的信号量,随后方法调用者会将这个任务单独放入队列,然后修改do_suspend_cnt标志保证后续while循环直接goto out,barrier block的任务执行完之后_dispatch_queue_class_invoke
会将do_suspend_cnt重置回去,所以barrier block之后的任务会继续执行。
1 | static inline void |
流程图:
总结
dispatch_async如何实现?分发到主队列与全局队列有什么区别,一定会新建线程执行任务么?
dispatch_async 会把任务添加到队列链表中,添加完成后唤醒队列,全局队列唤醒时会从线程池中取出可用线程,如果没有会新建线程,然后在线程中执行队列取出的任务;主队列会唤醒主线程的Runloop,然后在Runloop中通知GCD执行主队列提交的任务。
dispatch_sync 如何实现?
dispatch_sync 一般在当前线程执行,如果是主队列的任务还是会切换到主线程执行。它使用与线程绑定的信号量来实现串行执行的功能。
向串行队列提交同步任务- 如果队列无任务调用
_dispatch_barrier_sync_f_invoke
执行任务。 - 如果队列存在其他任务或被挂起,则调用
_dispatch_barrier_sync_f_slow
,并且调用线程对应的信号量进行wait
操作,等待该队列的任务执行完之后用信号量通知队列继续执行任务。向当前串行队列同步提交block时会进入这个方法,导致死锁
- 如果队列无任务调用
向并发队列提交同步任务
* 如果队列无任务调用`_dispatch_sync_f_invoke`执行任务。
* 如果队列存在其他任务,或者队列被挂起,或者有正在执行的任务,则调用`_dispatch_sync_f_slow` 函数,使用信号量等待。
dispatch_barrier_async 如何实现?
改变了block
vtable
的标记位,当执行到该block时,会修改队列的suspend_count,待之前的任务都执行完毕,才会执行barrier block,待barrier_block执行完恢复suspend_count,并执行后面的任务。
如果把Barrier block提交到global queue,执行效果与dispatch_async 一致,只有将Barrier blocks 提交到DISPATCH_QUEUE_CONCURRENT
属性创建的自定义队列时它才有效。
参考文档
深入浅出 GCD 之 dispatch_queue
深入理解GCD
我所理解的 iOS 并发编程
扒了扒libdispatch源码
从NSTimer的失效性谈起(二):关于GCD Timer和libdispatch
GCD源码分析1 —— 开篇
细说 GCD(Grand Central Dispatch)如何用