iOS源码解析: dispatch_once是如何做到的

在之前的一篇文章 iOS源码解析: NotificationCenter是如何实现的? 中,顺便介绍了在dispatch_once时使用跨线程操作而导致死锁的情况。本文基于dispatch_once的源码,进一步介绍一下iOS习以为常的单例模式。看似非常简单,不过实际要考虑下边几个关键点:

  1. 懒加载
  2. 线程安全
  3. 编译器指令重排优化
  4. 可继承、方法可override

Java的单例模式

最早接触的是Java中的几种单例写法,当时觉得非常神奇。一步步改进的过程值得好好思考。

1 lazy loading & 非线程安全

1
2
3
4
5
6
7
8
9
10
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton sharedInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

严格来说,这种非线程安全的方式,根本算不上单例。

2 lazy loading & 线程安全

1
2
3
4
5
6
7
8
9
10
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton sharedInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

加上synchronized,能够保证线程安全。但所有的sharedInstance使用都加了锁,效率低下。

3 non lazy loading & 线程安全

以上的lazy loading俗称懒汉模式,仅在使用到的时候才去初始化instance变量。

而下边的这种俗称饿汉模式,instance在类加载的时候就实例化了。

1
2
3
4
5
6
7
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton sharedInstance() {
return instance;
}
}

饿汉模式是线程安全的,但却失去了lazy loading的效果。有时候提前初始化一些不必要的实例对象,甚至会严重影响性能。

4 静态内部类 & 线程安全

1
2
3
4
5
6
7
8
9
10
public class Singleton {
private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
}

private Singleton() {}
public static final Singleton sharedInstance() {
return SingletonHolder.singleton;
}
}

这种方式引入了一个内部类,避免了在Singleton加载的时候就初始化一个实例对象。从而兼顾了lazy loading和线程安全。

5 枚举 & 线程安全

1
2
3
4
5
6
public enum Singleton {
INSTANCE;
public void myMethod() {
System.out.println("myMethod");
}
}

这种方式可以说是Java单例的终极写法,但却无法继承了。

6 lazy loading & 双重校验锁

基于方式2的优化版本,主要优化synchronized的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton sharedInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

这个双重校验很关键,尤其是内部的 if (instance == null) 同样是必不可少的。多线程同时调用sharedInstance,虽然有加锁,但加锁的代码块中如果没有双重校验,依然会执行初始化操作。

这种方式已经非常安全了,但依然会有极低概率出现问题。*instance = new Singleton();8 这句代码,并非是原子操作。实际上,这句代码做了以下三件事:

  1. 给instance分配一块内存
  2. 调用Singleton的构造函数来初始化一个实例A
  3. 将instance指向初始化的实例A,此时instance就不是null了

JVM的编译器存在执行重排的优化,使得以上的2和3的执行顺序可能会变,即最终执行顺序可能是1-2-3或1-3-2。如果是1-3-2,则3执行完毕、2未执行之前,这个临界状态是很危险的。这时的instance不是null,指向的是一块未初始化的内存区域。假设此时其他线程调用sharedInstance函数,刚好执行到了外层的 if (instance == null) 判断,instance非null,则将这个未初始化的内存返回了。

总结一下:对instance的写操作未完成,其他线程就对其进行了读操作。因此确保 instance的写操作 为原子操作即可。

7 volatile

volatile关键字的作用是禁止指令重排,对instance的写操作会有一个内存屏障。确保了6中的执行顺序始终为1-2-3。即

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton sharedInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

讲了这么多,实际可以根据使用场景选择 方式5或者方式7 即可。下边来看看iOS中的情况。

iOS中的单例模式

Objective-C

Objective-C中的单例写法如下,这个太常见了没什么可说的

1
2
3
4
5
6
7
8
9
10
11
12
@implementation MyObject

+ (instancetype)sharedInstance {
static MyObject *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[MyObject alloc] init];
});
return instance;
}

@end

Swift

Swift默认没有dispatch_once,可以使用static let即可实现单例。不过这样也就没有了lazy loading的效果,即饿汉模式。

1
2
3
4
class SwiftyMediator {
static let shared = SwiftyMediator()
private init() {}
}

而如果想在业务中使用dispatch_once的类似作用,可以采用如下方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public extension DispatchQueue {
private static var onceTokens = [String]()

class func once(token: String, block: () -> Void) {
objc_sync_enter(self)
defer { objc_sync_exit(self) }

if onceTokens.contains(token) {
return
}

onceTokens.append(token)
block()
}
}

dispatch_once的底层实现

dispatch_once的底层实现其实并不复杂:

1
2
3
4
5
void
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}
1
2
3
4
#define _dispatch_Block_invoke(bb) \
( (dispatch_function_t) ((struct Block_layout *)bb)->invoke )

typedef void (*dispatch_function_t)(void *_Nullable);

dispatch_function_t就是一个函数指针。_dispatch_Block_invoke(block) 实际上将block转为 struct Block_layout *,将其invoke函数转为dispatch_function_t函数指针。

dispatch_once_f

dispatch_once_f的主体流程就是一个if判断,可以简单理解为 首次if判断返回YES,进入执行;后来if判断返回NO,进入等待流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
dispatch_once_gate_t l = (dispatch_once_gate_t)val;

#if !DISPATCH_ONCE_INLINE_FASTPATH || DISPATCH_ONCE_USE_QUIESCENT_COUNTER
uintptr_t v = os_atomic_load(&l->dgo_once, acquire);
if (likely(v == DLOCK_ONCE_DONE)) {
return;
}
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
if (likely(DISPATCH_ONCE_IS_GEN(v))) {
return _dispatch_once_mark_done_if_quiesced(l, v);
}
#endif
#endif
if (_dispatch_once_gate_tryenter(l)) {
return _dispatch_once_callout(l, ctxt, func);
}
return _dispatch_once_wait(l);
}

在dispatch_once_f的最初,实际上有先判断 &l->dgo_once 地址中存储的值。显然若该值为DLOCK_ONCE_DONE,即为once已经执行过了,代码也就直接return了。而这个值DLOCK_ONCE_DONE在后续很多地方有用到。

1
2
3
4
uintptr_t v = os_atomic_load(&l->dgo_once, acquire);
if (likely(v == DLOCK_ONCE_DONE)) {
return;
}

os_atomic_load将 &l->dgo_once 地址存储的值加载到变量v中。如果该值不为DLOCK_ONCE_DONE,则第一次调用时,_dispatch_once_gate_tryenter(l) 可以进入,则执行 return _dispatch_once_callout(l, ctxt, func);。后续的调用,则执行 return _dispatch_once_wait(l);,这就是once的原理。

而它是如何保证多线程下的安全性和once特性呢,看一下_dispatch_once_gate_tryenter的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct dispatch_once_gate_s {
union {
dispatch_gate_s dgo_gate;
uintptr_t dgo_once;
};
} dispatch_once_gate_s, *dispatch_once_gate_t;

#define DLOCK_ONCE_UNLOCKED ((uintptr_t)0)
#define DLOCK_ONCE_DONE (~(uintptr_t)0)

static inline bool
_dispatch_once_gate_tryenter(dispatch_once_gate_t l)
{
return os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED,
(uintptr_t)_dispatch_lock_value_for_self(), relaxed);
}

DLOCK_ONCE_UNLOCKED与DLOCK_ONCE_DONE对应,分别代表dispatch_once执行前后的标记状态。

os_atomic_cmpxchg是一个 比较+交换 的原子操作。比较 &l->dgo_once 的值是否等于 DLOCK_ONCE_UNLOCKED,若是则将 (uintptr_t)_dispatch_lock_value_for_self() 赋值给 &l->dgo_once。即这个原子操作确保了dispatch_once的线程安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define DLOCK_OWNER_MASK			((dispatch_lock)0xfffffffc)

static inline dispatch_lock
_dispatch_lock_value_from_tid(dispatch_tid tid)
{
return tid & DLOCK_OWNER_MASK;
}

DISPATCH_ALWAYS_INLINE
static inline dispatch_lock
_dispatch_lock_value_for_self(void)
{
return _dispatch_lock_value_from_tid(_dispatch_tid_self());
}

(uintptr_t)_dispatch_lock_value_for_self() 的返回值在 _dispatch_lock_is_locked 函数中也同样用到,用于加锁。

_dispatch_once_wait

而对于非首次的执行,是如何等待,并返回该block执行后生成的sharedInstance对象呢?

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
void
_dispatch_once_wait(dispatch_once_gate_t dgo)
{
dispatch_lock self = _dispatch_lock_value_for_self();
uintptr_t old_v, new_v;
dispatch_lock *lock = &dgo->dgo_gate.dgl_lock;
uint32_t timeout = 1;

for (;;) {
os_atomic_rmw_loop(&dgo->dgo_once, old_v, new_v, relaxed, {
if (likely(old_v == DLOCK_ONCE_DONE)) {
os_atomic_rmw_loop_give_up(return);
}
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
if (DISPATCH_ONCE_IS_GEN(old_v)) {
os_atomic_rmw_loop_give_up({
os_atomic_thread_fence(acquire);
return _dispatch_once_mark_done_if_quiesced(dgo, old_v);
});
}
#endif
new_v = old_v | (uintptr_t)DLOCK_WAITERS_BIT;
if (new_v == old_v) os_atomic_rmw_loop_give_up(break);
});
if (unlikely(_dispatch_lock_is_locked_by((dispatch_lock)old_v, self))) {
DISPATCH_CLIENT_CRASH(0, "trying to lock recursively");
}
#if HAVE_UL_UNFAIR_LOCK
_dispatch_unfair_lock_wait(lock, (dispatch_lock)new_v, 0,
DLOCK_LOCK_NONE);
#elif HAVE_FUTEX
_dispatch_futex_wait(lock, (dispatch_lock)new_v, NULL,
FUTEX_PRIVATE_FLAG);
#else
_dispatch_thread_switch(new_v, flags, timeout++);
#endif
(void)timeout;
}
}

os_atomic_rmw_loop用于从操作系统底层获取状态,使用 os_atomic_rmw_loop_give_up 来执行返回操作。即不停查询 &dgo->dgo_once 的值,若变为DLOCK_ONCE_DONE,则调用 os_atomic_rmw_loop_give_up(return); 退出等待。

_dispatch_once_callout

首次进入dispatch_once,会执行_dispatch_once_callout的流程,即调用该block。传入的第三个参数func即为之前包装好的dispatch_function_t函数指针。

1
2
3
4
5
6
7
static void
_dispatch_once_callout(dispatch_once_gate_t l, void *ctxt,
dispatch_function_t func)
{
_dispatch_client_callout(ctxt, func);
_dispatch_once_gate_broadcast(l);
}

_dispatch_client_callout就是实际执行block操作的地方:

1
2
3
4
5
6
7
8
9
10
11
void
_dispatch_client_callout(void *ctxt, dispatch_function_t f)
{
_dispatch_get_tsd_base();
void *u = _dispatch_get_unwind_tsd();
if (likely(!u)) return f(ctxt);
_dispatch_set_unwind_tsd(NULL);
f(ctxt);
_dispatch_free_unwind_tsd();
_dispatch_set_unwind_tsd(u);
}

实际执行block即调用 f(ctxt); 函数。

Thread-specific data(TSD)是线程私有的数据,包含TSD的一些函数用于向线程(thread)对象中存储和获取数据。如CFRunLoopGetMain()函数,调用_CFRunLoopGet0(),在其中即利用了TSD接口从thread中得到runloop对象。

这里的 _dispatch_get_tsd_base(); 也获取线程的私有数据。而 _dispatch_get_unwind_tsd、_dispatch_set_unwind_tsd和_dispatch_free_unwind_tsd 看来就是用于确保 f(ctxt) 执行的线程安全。

_dispatch_once_gate_broadcast

猜测一下_dispatch_once_gate_broadcast的作用,应该就是在block执行完毕后修改上边的&l->dgo_once的值,即标记为dispatch_once已经执行过了,且广播出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
static inline void
_dispatch_once_gate_broadcast(dispatch_once_gate_t l)
{
dispatch_lock value_self = _dispatch_lock_value_for_self();
uintptr_t v;
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
v = _dispatch_once_mark_quiescing(l);
#else
v = _dispatch_once_mark_done(l);
#endif
if (likely((dispatch_lock)v == value_self)) return;
_dispatch_gate_broadcast_slow(&l->dgo_gate, (dispatch_lock)v);
}

_dispatch_once_mark_done函数中会调用os_atomic_xchg,这是一个原子操作,用于将 &dgo->dgo_once 地址存储的值,设置为 DLOCK_ONCE_DONE 。此时,once操作即被标记为已执行过了。

atomic_xchg:Swaps the old value stored at location p with new value given by val. Returns old value.

1
2
3
4
5
static inline uintptr_t
_dispatch_once_mark_done(dispatch_once_gate_t dgo)
{
return os_atomic_xchg(&dgo->dgo_once, DLOCK_ONCE_DONE, release);
}

dispatch_once的注意点

GCD经常会隐含一些容易导致异常甚至直接崩溃的坑,大多是不合理的使用引发的。翻墙挂了导致无法Google,其他搜索引擎真是垃圾。所以,后边提到的两个DISPATCH_CLIENT_CRASH场景,留待后续补充吧。

block中如果执行了主线程sync操作,则会导致死锁

iOS源码解析: NotificationCenter是如何实现的? 中,顺便介绍了在dispatch_once时使用跨线程操作而导致死锁的情况。

DISPATCH_CLIENT_CRASH(0, “trying to lock recursively”);

在_dispatch_once_wait中的for循环中有这样一段代码:

1
2
3
if (unlikely(_dispatch_lock_is_locked_by((dispatch_lock)old_v, self))) {
DISPATCH_CLIENT_CRASH(0, "trying to lock recursively");
}

使用如下代码可以触发这样的死锁场景。

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
@implementation SingletonA

+ (instancetype)sharedInstance {
static SingletonA *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[SingletonA alloc] init];
});
return instance;
}

- (instancetype)init
{
self = [super init];
if (self) {
[SingletonB sharedInstance];
}
return self;
}

@end

@implementation SingletonB

+ (instancetype)sharedInstance {
static SingletonB *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[SingletonB alloc] init];
});
return instance;
}

- (instancetype)init
{
self = [super init];
if (self) {
[SingletonA sharedInstance];
}
return self;
}

@end

错误信息如下:

1
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
1
2
3
4
libdispatch.dylib`_dispatch_once_wait.cold.1:
0x10e8d047b <+0>: leaq 0x5c11(%rip), %rcx ; "BUG IN CLIENT OF LIBDISPATCH: trying to lock recursively"
0x10e8d0482 <+7>: movq %rcx, 0x27cc7(%rip) ; gCRAnnotations + 8
-> 0x10e8d0489 <+14>: ud2

以上只是一个非常简单的模拟,实际场景当然不会这么写。但是要注意多层操作后可能的死锁。

DISPATCH_CLIENT_CRASH(cur, “lock not owned by current thread”);

在_dispatch_once_gate_broadcast中,有这样一句 _dispatch_gate_broadcast_slow(&l->dgo_gate, (dispatch_lock)v);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void
_dispatch_gate_broadcast_slow(dispatch_gate_t dgl, dispatch_lock cur)
{
if (unlikely(!_dispatch_lock_is_locked_by_self(cur))) {
DISPATCH_CLIENT_CRASH(cur, "lock not owned by current thread");
}

#if HAVE_UL_UNFAIR_LOCK
_dispatch_unfair_lock_wake(&dgl->dgl_lock, ULF_WAKE_ALL);
#elif HAVE_FUTEX
_dispatch_futex_wake(&dgl->dgl_lock, INT_MAX, FUTEX_PRIVATE_FLAG);
#else
(void)dgl;
#endif
}

参考资料

坚持原创技术分享,您的支持将鼓励我继续创作! So,来杯咖啡?