iOS多线程:GCD源码分析<五>dispatch-source

dispatch_source

##概述
Dispatch Source是BSD系统内核惯有功能kqueue的包装,kqueue是在XNU内核中发生各种事件时,在应用程序编程方执行处理的技术。它的CPU负荷非常小,尽量不占用资源。当事件发生时,Dispatch Source会在指定的Dispatch Queue中执行事件的处理。

使用

dispatch_source 最常见的用途是实现定时器,GCD timer不依赖runloop,因此任何线程都可以使用,由于使用block,不会忘记避免循环引用,定时器可以自由控制精度,随时修改时间间隔等。

1
2
3
4
5
6
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);  
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 0), 10*NSEC_PER_SEC, 1*NSEC_PER_SEC); //每10秒触发timer,误差1秒
dispatch_source_set_event_handler(timer, ^{
// 定时器触发时执行的 block
});
dispatch_resume(timer);

源码分析

dispatch_source_create

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dispatch_source_t dispatch_source_create(dispatch_source_type_t type,
uintptr_t handle,
unsigned long mask,
dispatch_queue_t q) {
//申请内存空间
ds = _dispatch_alloc(DISPATCH_VTABLE(source),
sizeof(struct dispatch_source_s));
//初始化ds
_dispatch_queue_init((dispatch_queue_t)ds);
ds->dq_label = "source";

ds->do_ref_cnt++; // the reference the manager queue holds
ds->do_ref_cnt++; // since source is created suspended
//默认处于暂状态,需要手动调用resume
ds->do_suspend_cnt = DISPATCH_OBJECT_SUSPEND_INTERVAL;
ds->do_targetq = &_dispatch_mgr_q;
// First item on the queue sets the user-specified target queue
//设置事件回调的队列 q是回调队列 _dispatch_mgr_q是由哪个队列来管理这个source
dispatch_set_target_queue(ds, q);
_dispatch_object_debug(ds, "%s", __func__);
return ds;
}

dispatch_source_set_timer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//实际调用方法
static inline void _dispatch_source_set_timer(dispatch_source_t ds, dispatch_time_t start,
uint64_t interval, uint64_t leeway, bool source_sync) {
//首先屏蔽非timer类型的source
if (slowpath(!ds->ds_is_timer) ||
slowpath(ds_timer(ds->ds_refs).flags & DISPATCH_TIMER_INTERVAL)) {
DISPATCH_CLIENT_CRASH("Attempt to set timer on a non-timer source");
}
//创建dispatch_set_timer_params结构体绑定source和timer参数
struct dispatch_set_timer_params *params;
params = _dispatch_source_timer_params(ds, start, interval, leeway);
_dispatch_source_timer_telemetry(ds, params->ident, &params->values);
dispatch_retain(ds);
if (source_sync) {
//将source当做队列使用,执行dispatch_barrier_async_f压入队列,
//核心函数为_dispatch_source_set_timer2
return _dispatch_barrier_trysync_f((dispatch_queue_t)ds, params,
_dispatch_source_set_timer2);
} else {
return _dispatch_source_set_timer2(params);
}
}

_dispatch_source_set_timer实际上是调用了_dispatch_source_set_timer2函数:

1
2
3
4
5
6
7
8
9
static void _dispatch_source_set_timer2(void *context) {
// Called on the source queue
struct dispatch_set_timer_params *params = context;
//暂停队列,避免修改过程中定时器被触发了。
dispatch_suspend(params->ds);
//在_dispatch_mgr_q队列上执行_dispatch_source_set_timer3(params)
dispatch_barrier_async_f(&_dispatch_mgr_q, params,
_dispatch_source_set_timer3);
}

_dispatch_source_set_timer2函数的逻辑是在_dispatch_mgr_q(序列号为2的manager queue)队列执行_dispatch_source_set_timer3(params),接下来的逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void _dispatch_source_set_timer3(void *context) {
// Called on the _dispatch_mgr_q
struct dispatch_set_timer_params *params = context;
dispatch_source_t ds = params->ds;
ds->ds_ident_hack = params->ident;
ds_timer(ds->ds_refs) = params->values;
ds->ds_pending_data = 0;
(void)dispatch_atomic_or2o(ds, ds_atomic_flags, DSF_ARMED, release);
//恢复队列,对应着_dispatch_source_set_timer2函数中的dispatch_suspend
dispatch_resume(ds);
// Must happen after resume to avoid getting disarmed due to suspension
//根据下一次触发时间将timer进行排序
_dispatch_timers_update(ds);
dispatch_release(ds);
if (params->values.flags & DISPATCH_TIMER_WALL_CLOCK) {
_dispatch_mach_host_calendar_change_register();
}
free(params);
}

当初提交到_dispatch_mgr_q队列的block会被执行,调用&dispatch_mgr_q->do_invoke 函数,即&_dispatch_mgr_qvtable中定义的_dispatch_mgr_thread。接下来会走到_dispatch_mgr_invoke函数。在这个函数里用I/O多路复用功能的select来实现定时器功能:

1
r = select(FD_SETSIZE, &tmp_rfds, &tmp_wfds, NULL, poll ? (struct timeval*)&timeout_immediately : NULL);

当内层的 _dispatch_mgr_q 队列被唤醒后,还会进一步唤醒外层的队列(当初用户指定的那个),并在指定队列上执行 timer 触发时的 block。

dispatch_source_set_event_handler/dispatch_source_set_cancel_handler

保存和取消事件处理的上下文信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void dispatch_source_set_event_handler(dispatch_source_t ds,
dispatch_block_t handler) {
//将block进行copy后压入到队列中
handler = _dispatch_Block_copy(handler);
_dispatch_barrier_trysync_f((dispatch_queue_t)ds, handler,
_dispatch_source_set_event_handler2);
}
static void _dispatch_source_set_event_handler2(void *context) {
dispatch_source_t ds = (dispatch_source_t)_dispatch_queue_get_current();
dispatch_assert(dx_type(ds) == DISPATCH_SOURCE_KEVENT_TYPE);
dispatch_source_refs_t dr = ds->ds_refs;

if (ds->ds_handler_is_block && dr->ds_handler_ctxt) {
Block_release(dr->ds_handler_ctxt);
}
//设置上下文,保存提交的block等信息
dr->ds_handler_func = context ? _dispatch_Block_invoke(context) : NULL;
dr->ds_handler_ctxt = context;
ds->ds_handler_is_block = true;
}

void dispatch_source_set_cancel_handler(dispatch_source_t ds,
dispatch_block_t handler) {
//将block进行copy后压入到队列中
handler = _dispatch_Block_copy(handler);
_dispatch_barrier_trysync_f((dispatch_queue_t)ds, handler,
_dispatch_source_set_cancel_handler2);
}
static void _dispatch_source_set_cancel_handler2(void *context) {
dispatch_source_t ds = (dispatch_source_t)_dispatch_queue_get_current();
dispatch_assert(dx_type(ds) == DISPATCH_SOURCE_KEVENT_TYPE);
dispatch_source_refs_t dr = ds->ds_refs;

if (ds->ds_cancel_is_block && dr->ds_cancel_handler) {
Block_release(dr->ds_cancel_handler);
}
//保存事件取消的信息
dr->ds_cancel_handler = context;
ds->ds_cancel_is_block = true;
}

dispatch_resume/dispatch_suspend
GCD 对象的暂停和恢复由 do_suspend_cnt 决定,暂停时通过原子操作将改属性的值加 2,对应的在恢复时通过原子操作将该属性减二。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//恢复
void dispatch_resume(dispatch_object_t dou) {
DISPATCH_OBJECT_TFB(_dispatch_objc_resume, dou);
// Global objects cannot be suspended or resumed.
if (slowpath(dou._do->do_ref_cnt == DISPATCH_OBJECT_GLOBAL_REFCNT) ||
slowpath(dx_type(dou._do) == DISPATCH_QUEUE_ROOT_TYPE)) {
return;
}
//将do_suspend_cnt原子性减二,并返回之前存储的值
unsigned int suspend_cnt = dispatch_atomic_sub_orig2o(dou._do,
do_suspend_cnt, DISPATCH_OBJECT_SUSPEND_INTERVAL, relaxed);
if (fastpath(suspend_cnt > DISPATCH_OBJECT_SUSPEND_INTERVAL)) {
return _dispatch_release(dou._do);
}
if (fastpath(suspend_cnt == DISPATCH_OBJECT_SUSPEND_INTERVAL)) {
_dispatch_wakeup(dou._do);
return _dispatch_release(dou._do);
}
DISPATCH_CLIENT_CRASH("Over-resume of an object");
}
//暂停
void dispatch_suspend(dispatch_object_t dou) {
DISPATCH_OBJECT_TFB(_dispatch_objc_suspend, dou);
if (slowpath(dou._do->do_ref_cnt == DISPATCH_OBJECT_GLOBAL_REFCNT) ||
slowpath(dx_type(dou._do) == DISPATCH_QUEUE_ROOT_TYPE)) {
return;
}
//将do_suspend_cnt原子性加二
(void)dispatch_atomic_add2o(dou._do, do_suspend_cnt,
DISPATCH_OBJECT_SUSPEND_INTERVAL, relaxed);
_dispatch_retain(dou._do);
}

do_suspend_cnt有两个默认值:

1
2
3
4
#define DISPATCH_OBJECT_SUSPEND_LOCK        1u
#define DISPATCH_OBJECT_SUSPEND_INTERVAL 2u
#define DISPATCH_OBJECT_SUSPENDED(x) \
((x)->do_suspend_cnt >= DISPATCH_OBJECT_SUSPEND_INTERVAL)

在唤醒队列时有如下代码:

1
2
3
4
5
6
7
void _dispatch_queue_invoke(dispatch_queue_t dq) {  
if (!dispatch_atomic_sub2o(dq, do_suspend_cnt, DISPATCH_OBJECT_SUSPEND_LOCK)) {
if (dq->dq_running == 0) {
_dispatch_wakeup(dq); // verify that the queue is idle
}
}
}

可见能够唤醒队列的前提是 dp->do_suspend_cnt - 1 = 0,也就是要求 do_suspend_cnt 的值就是 DISPATCH_OBJECT_SUSPEND_LOCK
观察 8 个全局队列和主队列的定义就会发现,他们的 do_suspend_cnt 值确实为 DISPATCH_OBJECT_SUSPEND_LOCK,因此默认处于启动状态。
dispatch_source 的 create 方法中,do_suspend_cnt 的初始值为 DISPATCH_OBJECT_SUSPEND_INTERVAL,因此默认处于暂停状态,需要手动开启。

dispatch_after

dispatch_after 是基于Dispatch Source的定时器实现的,函数内部直接调用dispatch_after_f,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void *ctxt, dispatch_function_t func) {
uint64_t delta, leeway;
dispatch_source_t ds;
//屏蔽DISPATCH_TIME_FOREVER类型
if (when == DISPATCH_TIME_FOREVER) {
#if DISPATCH_DEBUG
DISPATCH_CLIENT_CRASH(
"dispatch_after_f() called with 'when' == infinity");
#endif
return;
}
delta = _dispatch_timeout(when);
if (delta == 0) {
return dispatch_async_f(queue, ctxt, func);
}
leeway = delta / 10; // <rdar://problem/13447496>
if (leeway < NSEC_PER_MSEC) leeway = NSEC_PER_MSEC;
if (leeway > 60 * NSEC_PER_SEC) leeway = 60 * NSEC_PER_SEC;

// this function can and should be optimized to not use a dispatch source
//创建dispatch_source
ds = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_assert(ds);

dispatch_continuation_t dc = _dispatch_continuation_alloc();
dc->do_vtable = (void *)(DISPATCH_OBJ_ASYNC_BIT | DISPATCH_OBJ_BARRIER_BIT);
dc->dc_func = func;
dc->dc_ctxt = ctxt;
dc->dc_data = ds;
//将dispatch_continuation_t存储到上下文中
dispatch_set_context(ds, dc);
//设置timer并启动
dispatch_source_set_event_handler_f(ds, _dispatch_after_timer_callback);
dispatch_source_set_timer(ds, when, DISPATCH_TIME_FOREVER, leeway);
dispatch_resume(ds);
}

timer到时之后,会调用_dispatch_after_timer_callback函数,在这里取出上下文里的block并执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
void _dispatch_after_timer_callback(void *ctxt) {
dispatch_continuation_t dc = ctxt, dc1;
dispatch_source_t ds = dc->dc_data;
dc1 = _dispatch_continuation_free_cacheonly(dc);
//执行任务的block并执行
_dispatch_client_callout(dc->dc_ctxt, dc->dc_func);
//清理数据
dispatch_source_cancel(ds);
dispatch_release(ds);
if (slowpath(dc1)) {
_dispatch_continuation_free_to_cache_limit(dc1);
}
}

总结

Dispatch Source使用最多的就是用来实现定时器,source创建后默认是暂停状态,需要手动调用dispatch_resume启动定时器。dispatch_after只是封装调用了dispatch source定时器,然后在回调函数中执行定义的block。

source 最终会被提交到manager队列中,按照触发时间排好序。随后找到最近触发的定时器,调用内核的select方法等待。等待结束后,依次唤醒manager队列和用户制定队列,最终触发设置的回调block。
GCD中的对象用do_suspend_cnt来表示是否暂停。队列默认处于启动状态,而dispatch_source需要手动启动。

Dispatch Source定时器使用时也有一些需要注意的地方,不然很可能会引起crash:

1、循环引用:dispatch_source_set_event_handler 使用时要避免循环引用。

2、dispatch_resume和dispatch_suspend调用次数需要平衡,如果重复调用dispatch_resume则会崩溃,因为重复调用会让dispatch_resume代码里if分支不成立,从而执行了DISPATCH_CLIENT_CRASH(“Over-resume of an object”)导致崩溃。

3、source在suspend状态下,如果直接设置source = nil或者重新创建source都会造成crash。正确的方式是在resume状态下调用dispatch_source_cancel(source)后再重新创建。

参考资料

从NSTimer的失效性谈起(二):关于GCD Timer和libdispatch
深入浅出 GCD 之 dispatch_source
深入理解GCD