iOS源码解析: dispatch_once是如何实现的?

9,131 阅读8分钟

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

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

Java的单例模式

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

1 lazy loading & 非线程安全

public class Singleton {
	private static Singleton instance;
	private Singleton() {}
	public static Singleton sharedInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

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

2 lazy loading & 线程安全

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在类加载的时候就实例化了。

public class Singleton {
	private static Singleton instance = new Singleton();
	private Singleton() {}
	public static Singleton sharedInstance() {
		return instance;
	}
}

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

4 静态内部类 & 线程安全

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 枚举 & 线程安全

public enum Singleton {
	INSTANCE;
	public void myMethod() {
		System.out.println("myMethod");
	}
}

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

6 lazy loading & 双重校验锁

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

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。即

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中的单例写法如下,这个太常见了没什么可说的

@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的效果,即饿汉模式。

class SwiftyMediator {
    static let shared = SwiftyMediator()
	private init() {}
}

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

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的底层实现其实并不复杂:

void
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
	dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}
#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,进入等待流程

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在后续很多地方有用到。

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

如果该值不为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的实现:

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的线程安全。

#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对象呢?

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函数指针。

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操作的地方:

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已经执行过了,且广播出去。

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.

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循环中有这样一段代码:

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

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

@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

错误信息如下:

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
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);

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
}

参考资料