dispatch_once

之前这篇文章dispatch_semaphore搬运源码搬了dispatch_semaphore相关的方法,最近看了dispatch_once,来扒一扒。


看个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)viewDidLoad {
[super viewDidLoad];
[self once1];
}
- (void)once1 {
NSLog(@"Started dispatch_once1");
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self once2];
});
NSLog(@"Finished dispatch_once1");
}
- (void)once2 {
NSLog(@"Started dispatch_once2");
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self once1];
});
NSLog(@"Finished dispatch_once2");
}

想想这个代码会输出什么东西呢?那我们先看看 dispatch_once 是怎么实现的吧。


dispatch_once 实现

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
typedef long dispatch_once_t;
void dispatch_once(dispatch_once_t *val, dispatch_block_t block) {
void *ctxt = block;
dispatch_function_t func = _dispatch_Block_invoke(block);
_dispatch_once_waiter_t volatile *vval = (_dispatch_once_waiter_t*)val;
struct _dispatch_once_waiter_s dow = { }; // 多线程同时第一次进入时的排队队列
_dispatch_once_waiter_t tail = &dow, next, tmp;
dispatch_thread_event_t event;
// case 1: 第一次进入,这里判断vval的同时会有exchange操作,下次这个条件就不再成立了
if (os_atomic_cmpxchg(vval, NULL, tail, acquire) /*可以简单理解为 vval == NULL*/) {
dow.dow_thread = _dispatch_tid_self();
// 执行block内容
_dispatch_client_callout(ctxt, func);
// 标记为完成,将vval设置为DISPATCH_ONCE_DONE
vval = DISPATCH_ONCE_DONE;
// 唤醒其它线程的进入后的等待
while (next != tail) {
_dispatch_wait_until(tmp = (_dispatch_once_waiter_t)next->dow_next);
event = &next->dow_event;
next = tmp;
_dispatch_thread_event_signal(event); // 唤醒多线程第一次进入的等待
}
} else {
_dispatch_thread_event_init(&dow.dow_event);
next = *vval;
for (;;) {
// case 2: 第二次进入,当第一次初始化完成后,后面的访问都会进入到这个分支,因为vval被设置为DISPATCH_ONCE_DONE
if (vval == DISPATCH_ONCE_DONE) {
break;
}
// case 3: 其它多线程同时第一次进入,将会排队等待第一个进入的线程完成事件信号
if (os_atomic_cmpxchgvw(vval, next, tail, &next, release)) {
dow.dow_thread = next->dow_thread;
dow.dow_next = next;
if (dow.dow_thread) {
pthread_priority_t pp = _dispatch_get_priority();
_dispatch_thread_override_start(dow.dow_thread, pp, val);
}
// 原地等待最先第一次进入的线程执行完毕的信号
_dispatch_thread_event_wait(&dow.dow_event);
if (dow.dow_thread) {
_dispatch_thread_override_end(dow.dow_thread, val);
}
break;
}
}
_dispatch_thread_event_destroy(&dow.dow_event);
}
}

我们可以忽略那个for循环,可以看到有三种情况:

  • case 1: 第一次进入

    if (os_atomic_cmpxchg(vval, NULL, tail, acquire)

    第一次进入会直接执行传入的block,然后设置已执行过的标记,最后去唤醒 其它地方同时第一次进入的排队

  • case 2: 第一次完成之后进入

    if (vval == DISPATCH_ONCE_DONE) break;

    第一次完成之后进入时,会发现标记已经是 DISPATCH_ONCE_DONE,然后什么也没做。

  • case 3: 多处同时第一次进入

    这时会在 _dispatch_once_waiter_s dow 的尾部多加一个排队等待,主要是这里 _dispatch_thread_event_wait(&dow.dow_event)


问题的输出

最后看看最开始那个问题的输出吧:

1
2
3
4
2017-03-20 14:51:40.057 dispatch_once_demo[16124:719256] Started dispatch_once1
2017-03-20 14:51:40.057 dispatch_once_demo[16124:719256] Started dispatch_once2
2017-03-20 14:51:40.057 dispatch_once_demo[16124:719256] Started dispatch_once1
(lldb)

然后线程就卡住了,这可能还不够清楚到底发生了什么,那我们这时暂停程序看看调用栈是什么样吧:

dispatch_once_demo

可以发现执行卡在了 _dispatch_thread_event_wait_slow
代码第一次进入 once1case 1;然后进入了 once2case 1;这时又递归进入了 once1case 3 了。

这些都在同一线程,所以就卡住了,因为 case 3 卡住了当前线程在等待第一次进入 once1 的执行结束,而当前线程已经被卡住了,所以第一次进入的 once1 没有办法继续执行。

这里还有一个不明白的地方,没看明白 os_atomic_cmpxchg 是怎么实现的?


iOS Objective-C libdispatch GCD