In a previous article, iOS source parsing: How Is NotificationCenter implemented? In dispatch_once, the case of deadlock caused by using cross-thread operations at dispatch_once is described in passing. This article, based on the source code of Dispatch_once, takes a closer look at the usual singleton pattern of iOS. It may seem simple, but here are some key points to consider:
- Lazy loading
- Thread safety
- Compiler instruction reordering optimization
- Methods can be inherited and override can be used
Java singleton pattern
The earliest contact is Java in a few singletons, then feel very magical. The process of improving step by step is worth thinking about.
1 Lazy loading & Non-thread safe
public class Singleton {
private static Singleton instance;
private Singleton(a) {}
public static Singleton sharedInstance(a) {
if (instance == null) {
instance = new Singleton();
}
returninstance; }}Copy the code
Strictly speaking, this non-thread-safe approach is not a singleton at all.
2 Lazy loading & Thread safety
public class Singleton {
private static Singleton instance;
private Singleton(a) {}
public static synchronized Singleton sharedInstance(a) {
if (instance == null) {
instance = new Singleton();
}
returninstance; }}Copy the code
With synchronized, you can ensure thread safety. However, all sharedInstance uses are locked and inefficient.
3 Non lazy loading & Thread safety
In lazy loading, the instance variable is initialized only when it is used.
In the example below, instance is instantiated when the class is loaded.
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(a) {}
public static Singleton sharedInstance(a) {
returninstance; }}Copy the code
Han mode is thread-safe, but loses lazy loading. Sometimes unnecessary instance objects are initialized early, and performance can be severely affected.
4 Static inner classes & Thread-safe
public class Singleton {
private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
}
private Singleton(a) {}
public static final Singleton sharedInstance(a) {
returnSingletonHolder.singleton; }}Copy the code
This approach introduces an inner class that avoids initializing an instance object when the Singleton is loaded. Lazy loading and thread safety are both considered.
5 Enumeration & Thread safe
public enum Singleton {
INSTANCE;
public void myMethod(a) {
System.out.println("myMethod"); }}Copy the code
This is the ultimate way to write a Java singleton, but it’s not inherited.
6 Lazy loading & Double check lock
The optimized version based on Mode 2 mainly optimizes the use of synchronized:
public class Singleton {
private static Singleton instance;
private Singleton(a) {}
public static Singleton sharedInstance(a) {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = newSingleton(); }}}returninstance; }}Copy the code
This double-check is critical, especially as the internal if (instance == null) is also essential. SharedInstance is called by multiple threads at the same time, although the lock is added, but the locked block does not have double check, the initialization operation is still performed.
This is already very safe, but there is still a very low probability of problems. ***instance = new Singleton(); **8 this code is not an atomic operation. In fact, this code does three things:
- Example Allocate a block of memory to instance
- Call the constructor of Singleton to initialize an instance A
- Point instance to initialized instance A, and instance is no longer null
The JVM compiler is optimized for reordering, so that the order of execution of the above 2 and 3 May change, so that the final order of execution may be 1-2-3 or 1-3-2. If it is 1-3-2, this criticality is dangerous until 3 is executed and 2 is not executed. The instance is not null, but refers to an uninitialized memory region. If (instance == null) {if (instance == null);
To summarize: The write to instance is incomplete, and another thread reads it. So make sure that the write to instance is atomic.
7 volatile
The volatile keyword is used to prevent reordering of instructions and to create a memory barrier for writes to instance. This ensures that the execution sequence in 6 is always 1-2-3. namely
public class Singleton {
private static volatile Singleton instance;
private Singleton(a) {}
public static Singleton sharedInstance(a) {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = newSingleton(); }}}returninstance; }}Copy the code
Having said all that, you can actually choose option 5 or option 7 depending on the usage scenario. So let’s look at what happens in iOS.
Singletons in iOS
Objective-C
The singletons in Objective-C are written as follows, this is too common to say anything about, right
@implementation MyObject
+ (instancetype)sharedInstance {
static MyObject *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[MyObject alloc] init];
});
return instance;
}
@end
Copy the code
Swift
Swift does not have dispatch_once by default. You can implement singletons using static lets. However, there is no lazy loading effect.
class SwiftyMediator {
static let shared = SwiftyMediator(a)private init() {}}Copy the code
If you want to use a similar function of dispatch_once in a service, you can use the following methods:
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()
}
}
Copy the code
The underlying implementation of dispatch_once
The underlying implementation of dispatch_once isn’t that complicated:
void
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}
Copy the code
#define _dispatch_Block_invoke(bb) \
( (dispatch_function_t) ((struct Block_layout *)bb)->invoke )
typedef void (*dispatch_function_t)(void *_Nullable);
Copy the code
Dispatch_function_t is just a function pointer. ***_dispatch_Block_invoke(block)*** convert block to ***struct Block_layout **** and convert its invoke function to dispatch_function_t function pointer.
dispatch_once_f
The main process of dispatch_onCE_f is an if judgment, which can be simply understood as the first if judgment returns YES and enters the execution. Then the if judgment returns NO and the wait process enters.
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);
}
Copy the code
At the beginning of dispatch_once_f, there is actually a value stored in the &l-> dGO_once address. If the value is DLOCK_ONCE_DONE, it means that once has already been executed, and the code simply returns. This value, DLOCK_ONCE_DONE, will be useful in many subsequent places.
uintptr_t v = os_atomic_load(&l->dgo_once, acquire);
if (likely(v == DLOCK_ONCE_DONE)) {
return;
}
Copy the code
If this value is not DLOCK_ONCE_DONE, then ***_dispatch_once_gate_tryenter(l)*** can enter, ***return _dispatch_once_callout(l, CTXT, func); * * *. For subsequent calls, run ***return _dispatch_once_wait(l); ***, this is how once works.
To ensure security and the once feature for multiple threads, take a look at the implementation of _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);
}
Copy the code
DLOCK_ONCE_UNLOCKED and DLOCK_ONCE_DONE indicate the flag status before and after dispatch_once is executed.
Os_atomic_cmpxchg is a compare + swap atomic operation. Compare whether the value of &l->dgo_once is equal to DLOCK_ONCE_UNLOCKED. If it is, assign (uintptr_t)_dispatch_lock_value_for_self() to &l->dgo_once. This atomic operation ensures that dispatch_once is thread safe.
#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());
}
Copy the code
The return value of (uintptr_t)_dispatch_lock_value_for_self() is also used in the _dispatch_lock_is_locked function for locking.
_dispatch_once_wait
For a non-initial execution, how do you wait and return the sharedInstance object that was generated after the block was executed?
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; }}Copy the code
Os_atomic_rmw_loop is used to obtain the status from the underlying operating system, and OS_ATOMic_RMw_loop_give_up is used to return the status. Os_atomic_rmw_loop_give_up (return); dgo->dgo_once (); Exit waiting.
_dispatch_once_callout
When dispatch_once is entered for the first time, the _dispatch_onCE_callout process is executed, that is, the block is invoked. The third argument passed in, func, is a pointer to the previously wrapped dispatch_function_t function.
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);
}
Copy the code
_dispatch_client_callout is where the actual block operation is performed:
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);
}
Copy the code
To actually execute a block is to call f(CTXT); Function.
Thread-specific Data (TSD) is thread-private data that contains TSD functions for storing and retrieving data from Thread objects. For example, the CFRunLoopGetMain() function calls _CFRunLoopGet0(), where the TSD interface is used to get the Runloop object from thread.
Here the _dispatch_get_tsd_base (); Also get private data for the thread. _dispatch_get_unwind_tsd, _dispatch_set_unwind_tsd, and _dispatch_free_unwind_tsd appear to be thread safe for f(CTXT) execution.
_dispatch_once_gate_broadcast
After the block is executed, change the value of &l->dgo_once to indicate that “dispatch_once” has been executed and that “dispatch_once” has been broadcast.
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);
}
Copy the code
The _dispatch_once_mark_done function calls OS_atomic_xchg, which is an atomic operation that sets the value stored at &dgo-> dGO_once to DLOCK_ONCE_DONE. At this point, the once operation is marked as executed.
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);
}
Copy the code
Note for dispatch_once
GCD often implies craters that can easily lead to exceptions or even outright crashes, mostly caused by inappropriate use. I can’t Google. Other search engines are rubbish. Therefore, the two DISPATCH_CLIENT_CRASH scenarios mentioned below will be added later.
Block causes a deadlock if the main thread sync operation is performed
Source parsing in iOS: How is NotificationCenter implemented? In dispatch_once, the case of deadlock caused by using cross-thread operations at dispatch_once is described in passing.
DISPATCH_CLIENT_CRASH(0, “trying to lock recursively”);
In the for loop of _dispatch_once_wait we have this code:
if (unlikely(_dispatch_lock_is_locked_by((dispatch_lock)old_v, self))) {
DISPATCH_CLIENT_CRASH(0."trying to lock recursively");
}
Copy the code
Use the following code to trigger such a deadlock scenario.
@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
Copy the code
The error message is:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
Copy the code
libdispatch.dylib`_dispatch_once_wait.cold1.:
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
Copy the code
This is a very simple simulation, but of course the actual scenario is not written like this. But be aware of possible deadlocks after multiple operations.
DISPATCH_CLIENT_CRASH(cur, “lock not owned by current thread”);
_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
}
Copy the code
The resources
- Libdispatch – 1008.220.2
- Dispatch
- GCD Internals
- Simple simple GCD dispatch_once
- atomic_xchg