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:

  1. Lazy loading
  2. Thread safety
  3. Compiler instruction reordering optimization
  4. 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:

  1. Example Allocate a block of memory to instance
  2. Call the constructor of Singleton to initialize an instance A
  3. 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