在之前的一篇文章 iOS源码解析: NotificationCenter是如何实现的? 中,顺便介绍了在dispatch_once时使用跨线程操作而导致死锁的情况。本文基于dispatch_once的源码,进一步介绍一下iOS习以为常的单例模式。看似非常简单,不过实际要考虑下边几个关键点:
- 懒加载
- 线程安全
- 编译器指令重排优化
- 可继承、方法可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 这句代码,并非是原子操作。实际上,这句代码做了以下三件事:
- 给instance分配一块内存
- 调用Singleton的构造函数来初始化一个实例A
- 将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
}
参考资料
- libdispatch-1008.220.2
- Dispatch
- GCD Internals
- 深入浅出 GCD 之 dispatch_once
- atomic_xchg