在之前的一篇文章 iOS源码解析: 通知机制是如何实现的? 中,顺便介绍了在dispatch_once时使用跨线程操作而导致死锁的情况。本文基于dispatch_once的源码,进一步介绍一下iOS习以为常的单例模式。看似非常简单,不过实际要考虑下边几个关键点:
最早接触的是Java中的几种单例写法,当时觉得非常神奇。一步步改进的过程值得好好思考。
public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton sharedInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } 复制代码
严格来说,这种非线程安全的方式,根本算不上单例。
public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton sharedInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } 复制代码
加上synchronized,能够保证线程安全。但所有的sharedInstance使用都加了锁,效率低下。
以上的lazy loading俗称懒汉模式,仅在使用到的时候才去初始化instance变量。
而下边的这种俗称饿汉模式,instance在类加载的时候就实例化了。
public class Singleton { private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton sharedInstance() { return instance; } } 复制代码
饿汉模式是线程安全的,但却失去了lazy loading的效果。有时候提前初始化一些不必要的实例对象,甚至会严重影响性能。
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和线程安全。
public enum Singleton { INSTANCE; public void myMethod() { System.out.println("myMethod"); } } 复制代码
这种方式可以说是Java单例的终极写法,但却无法继承了。
基于方式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 这句代码,并非是原子操作。实际上,这句代码做了以下三件事:
JVM的编译器存在执行重排的优化,使得以上的2和3的执行顺序可能会变,即最终执行顺序可能是1-2-3或1-3-2。如果是1-3-2,则3执行完毕、2未执行之前,这个临界状态是很危险的。这时的instance不是null,指向的是一块未初始化的内存区域。假设此时其他线程调用sharedInstance函数,刚好执行到了外层的 if (instance == null) 判断,instance非null,则将这个未初始化的内存返回了。
总结一下:对instance的写操作未完成,其他线程就对其进行了读操作。因此确保 instance的写操作 为原子操作即可。
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中的情况。
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默认没有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的底层实现其实并不复杂:
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的主体流程就是一个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 函数中也同样用到,用于加锁。
而对于非首次的执行,是如何等待,并返回该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,会执行_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的作用,应该就是在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); } 复制代码
GCD经常会隐含一些容易导致异常甚至直接崩溃的坑,大多是不合理的使用引发的。翻墙挂了导致无法Google,其他搜索引擎真是垃圾。所以,后边提到的两个DISPATCH_CLIENT_CRASH场景,留待后续补充吧。
在 iOS源码解析: 通知机制是如何实现的? 中,顺便介绍了在dispatch_once时使用跨线程操作而导致死锁的情况。
在_dispatch_once_wait中的for循环中有这样一段代码:
if (unlikely(_dispatch_lock_is_locked_by((dispatch_lock)old_v, self))) { DISPATCH_CLIENT_CRASH(0, "trying to lock recursively"); } 复制代码
在_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 } 复制代码