Fun dispatch_once

Foreword

Speaking dispatch_once, the first thought might be a single case, such as the commonly used AFNetworking is so written:

+ (instancetype)sharedManager {
    static AFNetworkReachabilityManager *_sharedManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedManager = [self manager];
    });

    return _sharedManager;
}

But why write so you can ensure dispatch_once performed only once in the block? dispatch_once principle is kind of how? Is it possible to make dispatch_once in block execution times?

Basic knowledge

  • dispatch_once_t
typedef long dispatch_once_t;

dispatch_once_t fact, long type

  • DISPATCH_NOESCAPE
#if __has_attribute(noescape)
#define DISPATCH_NOESCAPE __attribute__((__noescape__))
#else
#define DISPATCH_NOESCAPE
#endif

void dispatch_once(dispatch_once_t *predicate, DISPATCH_NOESCAPE dispatch_block_t block);

DISPATCH_NOESCAPEAnd more used to modify block, it used to indicate the block executed before the end of the execution of the current method. Swift in similar @noescape(non-slip closure), the corresponding @escaping(escape closures). Briefly, the closure at the end of the called function is non-escape closures, closures for the escape of closure after the end of the function call.

  • dispatch_block_t
typedef void (^dispatch_block_t)(void);

The return value is a void, the void is the parameter block

  • dispatch_function_t
typedef void (*dispatch_function_t)(void *_Nullable);

The return value is void, the parameter is void *a function pointer

  • _dispatch_Block_invoke(bb)
#define _dispatch_Block_invoke(bb) \
        ((dispatch_function_t)((struct Block_layout *)bb)->invoke)

Function pointer in the structure Block_layout invoke. Here it comes to block structure, this text does not delve into, bearing in mind the specific implementation can invoke to block.

  • dispatch_once_gate_t
typedef struct dispatch_once_gate_s {
    union {
        dispatch_gate_s dgo_gate;
        uintptr_t dgo_once;
    };
} dispatch_once_gate_s, *dispatch_once_gate_t;

typedef struct dispatch_gate_s {
    dispatch_lock dgl_lock;
} dispatch_gate_s, *dispatch_gate_t;

typedef uint32_t dispatch_lock;

dispatch_once_gate_t to point to a structure pointer dispatch_once_gate_s

  • DISPATCH_DECL
    just mentioned dispatch_once_gate_t and dispatch_once_gate_s, incidentally with related DISPATCH_DECL
#define DISPATCH_DECL(name) typedef struct name##_s *name##_t

If you write, after DISPATCH_DECL (dispatch_once_gate), expanded to become

typedef struct dispatch_once_gate_s *dispatch_once_gate_t

This is in fact in the above statement, to ensure that the compiler can.
There are many similar statements in the source code, such as:

DISPATCH_DECL(dispatch_group);
DISPATCH_DECL(dispatch_queue);
  • DLOCK_ONCE_DONE
#define DLOCK_ONCE_DONE     (~(uintptr_t)0)
typedef unsigned long       uintptr_t;

0 of bitwise, the value of -1 into DLOCK_ONCE_DONE (Computer Basics: source, the inverted, complement)

  • DLOCK_ONCE_UNLOCKED
#define DLOCK_ONCE_UNLOCKED ((uintptr_t)0)

DLOCK_ONCE_UNLOCKED value of 0

Simple analytical

void dispatch_once(dispatch_once_t *val, dispatch_block_t block) {
    dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}

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);
}

You can see, the process is very simple.

  • v == DLOCK_ONCE_DONE
    as v == DLOCK_ONCE_DONE, the direct return. At this time corresponds to a single embodiment has been initialized, it will not block the implementation of

  • _dispatch_once_gate_tryenter

DISPATCH_ALWAYS_INLINE
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);
}

This is a type of return value bool inline functions, when the return value is true, the corresponding execution block.

  • _dispatch_once_wait
    case block currently being executed, such as the corresponding scenes, multi-threaded access to a single embodiment, a thread access, block has not been performed, then execution block. At the same time, thread 2 also access a single case, because the thread 1block execution has not been completed, so take _dispatch_once_wait logic wait until the thread is finished 1block (here more strictly, _dispatch_once_gate_broadcast is not executed, instead of not executing the block to facilitate understanding, here a direct say block, will be introduced later on _dispatch_once_gate_broadcast)

Detailed analysis

  • v == DLOCK_ONCE_DONE

Question: Why can compare v and DLOCK_ONCE_DONE performed to determine whether the block?
The first problem is to simplify the following code (not disclosed because these types, the data structures used to simulate the rewriting here):

typedef uint32_t jk_dispatch_lock;

typedef struct jk_dispatch_gate_s {
    jk_dispatch_lock dgl_lock;
} jk_dispatch_gate_s, *jk_dispatch_gate_t;

typedef struct jk_dispatch_once_gate_s {
    union {
        jk_dispatch_gate_s dgo_gate;
        uintptr_t dgo_once;
    };
} jk_dispatch_once_gate_s, *jk_dispatch_once_gate_t;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_once_t token = 1;
    dispatch_once_t *val = &token;
    jk_dispatch_once_gate_t l = (jk_dispatch_once_gate_t)val;
    NSLog(@"%ld", l->dgo_once);
}

It can be found, l-> dgo_once always equal token. Val at the address as the token, token address so directed l, l-> dgo_once is accessible to the token value. So, gcd source of l-> dgo_once is when the assigned? This comes to the second condition _dispatch_once_gate_tryenter

  • _dispatch_once_gate_tryenter
DISPATCH_ALWAYS_INLINE
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);
}

#define os_atomic_cmpxchg(p, e, v, m) \
        ({ _os_atomic_basetypeof(p) _r = (e); \
        atomic_compare_exchange_strong_explicit(_os_atomic_c11_atomic(p), \
        &_r, v, memory_order_##m, memory_order_relaxed); })

Can be seen, the final call atomic_compare_exchange_strong_explicit, this brief function (atomic):
1.l-> dgo_once and DLOCK_ONCE_UNLOCKED equal, then the _dispatch_lock_value_for_self () assigned to l-> dgo_once, and returns true;

  1. l-> dgo_once and DLOCK_ONCE_UNLOCKED range, it will DLOCK_ONCE_UNLOCKED assigned to l-> dgo_once, and returns false

Usually singleton write:

+ (instancetype)shared {
    static xx *one;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        one = [[self alloc] init];
    });
    return one;
}

At this value onceToken defaults to 0, so the first time you perform shared method call _dispatch_once_gate_tryenter return value is true (the above-mentioned DLOCK_ONCE_UNLOCKED is 0), then onceToken is assigned _dispatch_lock_value_for_self (). But _dispatch_lock_value_for_self () does not mean DLOCK_ONCE_DONE, then how to ensure the block is executed only once? One reason next function _dispatch_once_callout

  • _dispatch_once_callout
DISPATCH_NOINLINE
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);
}

The first call shared methods _dispatch_once_gate_tryenter onceToken will set the value of _dispatch_lock_value_for_self () and returns true, it will call _dispatch_once_callout, _dispatch_client_callout is used to call the block (how to call below will parse), while _dispatch_once_gate_broadcast will change the value of onceToken

DISPATCH_ALWAYS_INLINE
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_ALWAYS_INLINE
static inline uintptr_t
_dispatch_once_mark_done(dispatch_once_gate_t dgo)
{
    return os_atomic_xchg(&dgo->dgo_once, DLOCK_ONCE_DONE, release);
}

#define os_atomic_xchg(p, v, m) \
        atomic_exchange_explicit(_os_atomic_c11_atomic(p), v, memory_order_##m)

Visible, calls _dispatch_once_mark_done assigned to v, v and then compare the value of _dispatch_lock_value_for_self () of. Internal _dispatch_once_mark_done call os_atomic_xchg, brief function (atomic):
The DLOCK_ONCE_DONE assigned to dgo-> dgo_once, and returns dgo-> dgo_once original value (before being assigned value)
so in this case l-> dgo_once value DLOCK_ONCE_DONE, i.e. onceToken value DLOCK_ONCE_DONE (-1). The value of v is _dispatch_lock_value_for_self (), so in this case is equal to v value_self, _dispatch_once_gate_broadcast function return.

When the shared method is called again, because onceToken value DLOCK_ONCE_DONE, so a direct return, so the block will not be executed again.

  • _dispatch_client_callout
    now look back and say how said block is executed
void _dispatch_client_callout(void *ctxt, dispatch_function_t f) {
    @try {
        return f(ctxt);
    }
    @catch (...) {
        objc_terminate();
    }
}

It does not seem easy to understand, simplify the code will be as follows:

typedef void(*JK_BlockInvokeFunction)(void *, ...);

struct JK_Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    JK_BlockInvokeFunction invoke;
//    struct Block_descriptor_1 *descriptor;
        // imported variables
};

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_block_t block = ^{
        NSLog(@"执行");
    };
    
    void *ctxt = (__bridge void *)(block);
    struct JK_Block_layout *layout = (__bridge struct JK_Block_layout *)block;
    dispatch_function_t func = (dispatch_function_t)(layout->invoke);
    func(ctxt);
}

Console output 执行words, said above, layout-> invoke the practical realization of the block, func is a function pointer, then how to call this function, is clearly behind the increase ()will be implemented, but this block and no arguments, why should func afferent ctxt? Written before, if not understand, you can read my article: Powerful NSInvocation

In fact, there is a hidden parameter block target, and this target is the block itself, so execute func (ctxt) is equivalent to executing block ()

  • _dispatch_once_wait
    above said, when multi-threaded access, may perform _dispatch_once_wait, interested can look at the source code, there is not much difficult point here is not to resolve

Verification of the correctness of the control variables onceToken

@implementation Test
+ (instancetype)shared {
    static Test *t;
    static dispatch_once_t onceToken;
    NSLog(@"before: %ld", onceToken);
    dispatch_once(&onceToken, ^{
        t = [[Test alloc] init];
        NSLog(@"middle: %ld", onceToken);
    });
    NSLog(@"after: %ld", onceToken);
    return t;
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@", [Test shared]);
    NSLog(@"%@", [Test shared]);
    NSLog(@"%@", [Test shared]);
}

Console output is as follows:


329694-30d51f341d740b8a.png
Screen shot 2019-05-24 18.16.13.png

And before fully consistent interpretation of the source

If the initial value is set to -1 will happen?

+ (instancetype)shared {
    static Test *t;
    static dispatch_once_t onceToken = -1;
    NSLog(@"before: %ld", onceToken);
    dispatch_once(&onceToken, ^{
        t = [[Test alloc] init];
        NSLog(@"middle: %ld", onceToken);
    });
    NSLog(@"after: %ld", onceToken);
    return t;
}
329694-efab9dd233a70a3e.png
Screen shot 2019-05-24 18.27.22.png

We can see, block will not be executed, the returned value is null, also in line with the above analysis.

What happens if neither a set nor a value of -1 to 0? For example set to 1, the program will find Crash, and is positioned as shown:


329694-71f9983e84c513a9.png
Screen shot 2019-05-24 18.30.30.png

Analysis again: Call _dispatch_once_gate_tryenter first execution since onceToken initial value is 1, it returns false and 0 is assigned to onceToken, returns false impossible to execute _dispatch_once_callout, so the block will not be executed, onceToken will not be assigned to -1 . But directly execute _dispatch_once_wait, causing crash

How to block execution times

Having said that earlier, now play little tricks, let's block dispatch_once performed multiple times (such Singleton becomes ineffective)

By the above analysis is not difficult to find, block whether in fact is a value onceToken to control, so start from here, the code is written like this:

static dispatch_once_t onceToken;
@implementation Test
+ (instancetype)shared {
    static Test *t;
//    static dispatch_once_t onceToken;
    NSLog(@"before: %ld", onceToken);
    dispatch_once(&onceToken, ^{
        t = [[Test alloc] init];
        NSLog(@"middle: %ld", onceToken);
    });
    NSLog(@"after: %ld", onceToken);
    return t;
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@--%ld", [Test shared], onceToken);
    onceToken = 0;
    NSLog(@"%@--%ld", [Test shared], onceToken);
    onceToken = 0;
    NSLog(@"%@--%ld", [Test shared], onceToken);
}

Console output is as follows:


329694-016ba2b30216669b.png
Screen shot 2019-05-24 18.20.24.png

We can see, block is executed three times, and each time the return of Test instances are not the same


2019.05.29 update:
chance to see the Internet was written dispatch_once problems caused deadlock mix calls, the article analyzes the feeling did not say a pass to the point where a simple analysis of the next reasons:

@interface TestA : NSObject
+ (instancetype)shared;
@end

@interface TestB : NSObject
+ (instancetype)shared;
@end

@implementation TestA
+ (instancetype)shared {
    static TestA *a;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        a = [[self alloc] init];
    });
    return a;
}

- (instancetype)init {
    if (self = [super init]) {
        [TestB shared];
    }
    return self;
}
@end

@implementation TestB
+ (instancetype)shared {
    static TestB *b;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        b = [[self alloc] init];
    });
    return b;
}

- (instancetype)init {
    if (self = [super init]) {
        [TestA shared];
    }
    return self;
}
@end


- (void)viewDidLoad {
    [super viewDidLoad];
    
    [TestA shared];
}

It can be seen when the shared method call TestA internal TestB A will call the shared method, and another internal call A. B Through the above analysis, the first call, will onceToken set _dispatch_lock_value_for_self (), this operation before executing block, block, when executed, calls internal block B and B calls internal to A, is a onceToken A is neither 0 nor -1 value, so go wait logic, resulting in block B can not be finished, and the block B of the block a can not be finished lead can not be executed, to wait for each other is formed here, thereby causing crash


Have fun!

Guess you like

Origin blog.csdn.net/weixin_33749242/article/details/90771279