iOS多线程:GCD源码分析<三>dispatch_once

dispatch_once

概述

dispatch_once能保证任务只会被执行一次,即使同时多线程调用也是线程安全的。常用于创建单例、swizzeld method等功能。

使用

1
2
3
4
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//创建单例、method swizzled或其他任务
});

源码分析

1
2
3
4
//调用dispatch_once_f来处理
void dispatch_once(dispatch_once_t *val, dispatch_block_t block) {
dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}

封装dispatch_once_f函数,通过_dispatch_Block_invoke来执行block,它的定义如下:

1
2
3
//invoke是指触发block的具体实现,感兴趣的可以看一下Block_layout的结构体
#define _dispatch_Block_invoke(bb) \
((dispatch_function_t)((struct Block_layout *)bb)->invoke)

dispatch_once_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
37
38
void dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func) {
struct _dispatch_once_waiter_s * volatile *vval =
(struct _dispatch_once_waiter_s**)val;
struct _dispatch_once_waiter_s dow = { NULL, 0 };
struct _dispatch_once_waiter_s *tail, *tmp;
_dispatch_thread_semaphore_t sema;

if (dispatch_atomic_cmpxchg(vval, NULL, &dow, acquire)) {
_dispatch_client_callout(ctxt, func);

dispatch_atomic_maximally_synchronizing_barrier();
// above assumed to contain release barrier
tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE, relaxed);
tail = &dow;
while (tail != tmp) {
while (!tmp->dow_next) {
dispatch_hardware_pause();
}
sema = tmp->dow_sema;
tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next;
_dispatch_thread_semaphore_signal(sema);
}
} else {
dow.dow_sema = _dispatch_get_thread_semaphore();
tmp = *vval;
for (;;) {
if (tmp == DISPATCH_ONCE_DONE) {
break;
}
if (dispatch_atomic_cmpxchgvw(vval, tmp, &dow, &tmp, release)) {
dow.dow_next = tmp;
_dispatch_thread_semaphore_wait(dow.dow_sema);
break;
}
}
_dispatch_put_thread_semaphore(dow.dow_sema);
}
}
1
2
3
4
5
//构造的链表
struct _dispatch_once_waiter_s {
truevolatile struct _dispatch_once_waiter_s *volatile dow_next;
true_dispatch_thread_semaphore_t dow_sema;
};

分三种情况进行分析:

  1. 只有一个线程调用:此时传入的onceToken为空指针,if判断成立。通过_dispatch_client_callout执行block,然后将vval的值设为DISPATCH_ONCE_DONE,表示任务已完成,同时调用tmp保存先前的vval。此时,dow也为空,while判断不成立,代码执行结束。
  2. 同一线程第二次调用,此时vval已经变为了DISPATCH_ONCE_DONE,因此 if 判断不成立,进入 else 分支的 for 循环。由于 tmp 就是DISPATCH_ONCE_DONE,所以循环退出,没有做任何事。
  3. 多个线程同时调用,由于 if 判断中是一个原子性操作,所以必然只有一个线程能进入 if 分支,其他的进入 else 分支。由于其他线程在调用函数时,vval 还不是 DISPATCH_ONCE_DONE, 进入到for循环后半部分,这里构造了一个链表,链表的每个节点上都调用了信号量的 wait 方法并阻塞,而在 if 分支中,则会依次遍历所有的节点并调用 signal 方法,唤醒所有等待中的信号量。

总结

dispatch_once用原子性操作block执行完成标记位,同时用信号量确保只有一个线程执行block,等block执行完再唤醒所有等待中的线程。