你不知道的dispatch_once

作者: jiayoubaobao | 来源:发表于2016-07-31 00:47 被阅读764次
    - (void)viewDidLoad {
        [super viewDidLoad];
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [self viewDidLoad];
        });
    }
    

    这样调用会发生什么?
    主线程挂起,应用程序没有响应,Why
    我们刨根问底一下,自然会想到,dispatch_once是怎样实现的。

    我们知道,dispatch_once的作用是让block只执行一次,并且支持多线程,那么当多个线程同时调用dispatch_once函数会怎么样呢?


    Sequence Process1-6.png

    我么看到,每次进来一个线程,都会生成一个struct

    struct _dispatch_once_waiter_s {
        volatile struct _dispatch_once_waiter_s *volatile dow_next;
        _dispatch_thread_semaphore_t dow_sema;
    };
    

    其中dow_sema是用来存储当前线程的信号量,通过如下方式

     dow.dow_sema = _dispatch_get_thread_semaphore();
    

    dow_next则指向了下一个结构体变量(下一个进来的线程),当然,链表的tail节点,粗略的说也就是第一个dow结构体的dow_sema和dow_next都为null。


    Sequence Process1-7.png

    当block执行完, 通过

    dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE)
    

    会将onceToken(vval)标记成block执行完成状态,同时返回链表的head节点(因为之前,vval是用来当做指向头结点指针用的,因此只要* vval一下即可),onceToken即能当标记位又能当指针,真是又当爹又当妈啊。
    每次释放哨兵的信号量的时候,也就是之前被阻塞的线程可以继续运行。

    dispatch_atomic_cmpxchg(vval, tmp, &dow)
    

    原子操作,比较tmp和&dow是否相等。

    有了这些储备,我们解释一下上面的现象,当第一次dispatch_once执行的时候,在执行block的时候,会再次进入dispatch_once函数,并且生成一个dow(_dispatch_once_waiter_s)结构体,而此时onceToken还没有标记为完成状态,并且新的dow结构体和第一次的dow不相等,这个结构体会获取主线程的信号量,紧接着,执行wait,然后应用程序挂起。对应部分代码如下

                if (tmp == DISPATCH_ONCE_DONE) {
                    break;
                }
                dispatch_atomic_store_barrier();
                // 14. 如果当前dispatch_once执行的block没有结束,那么就将这些
                // 后续请求添加到链表当中
                if (dispatch_atomic_cmpxchg(vval, tmp, &dow)) {
                    dow.dow_next = tmp;
                    _dispatch_thread_semaphore_wait(dow.dow_sema);
                }
    

    还有个有趣的问题
    1.当家可以试试直接给onceToken写成~0l,看看会发生什么?

    下面是dispatch_once的源码

    #include "internal.h"
    
    #undef dispatch_once
    #undef dispatch_once_f
    
    struct _dispatch_once_waiter_s {
        volatile struct _dispatch_once_waiter_s *volatile dow_next;
        _dispatch_thread_semaphore_t dow_sema;
    };
    
    #define DISPATCH_ONCE_DONE ((struct _dispatch_once_waiter_s *)~0l)
    
    
    #ifdef __BLOCKS__
    // 1. 我们的应用程序调用的入口
    void
    dispatch_once(dispatch_once_t *val, dispatch_block_t block)
    {
        struct Block_basic *bb = (void *)block;
    
        // 2. 内部逻辑
        dispatch_once_f(val, block, (void *)bb->Block_invoke);
    }
    #endif
    
    DISPATCH_NOINLINE
    void
    dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
    {
        struct _dispatch_once_waiter_s * volatile *vval =
                (struct _dispatch_once_waiter_s**)val;
    
        // 3. 地址类似于简单的哨兵位
        struct _dispatch_once_waiter_s dow = { NULL, 0 };
    
        // 4. 在Dispatch_Once的block执行期进入的dispatch_once_t更改请求的链表
        struct _dispatch_once_waiter_s *tail, *tmp;
    
        // 5.局部变量,用于在遍历链表过程中获取每一个在链表上的更改请求的信号量
        _dispatch_thread_semaphore_t sema;
    
        // 6. Compare and Swap(用于首次更改请求)
        if (dispatch_atomic_cmpxchg(vval, NULL, &dow)) {
            dispatch_atomic_acquire_barrier();
    
            // 7.调用dispatch_once的block
            _dispatch_client_callout(ctxt, func);
    
            dispatch_atomic_maximally_synchronizing_barrier();
            //dispatch_atomic_release_barrier(); // assumed contained in above
    
            // 8. 更改请求成为DISPATCH_ONCE_DONE(原子性的操作)
            tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE);
            tail = &dow;
    
            // 9. 发现还有更改请求,继续遍历
            while (tail != tmp) {
    
                // 10. 如果这个时候tmp的next指针还没更新完毕,等一会
                while (!tmp->dow_next) {
                    _dispatch_hardware_pause();
                }
    
                // 11. 取出当前的信号量,告诉等待者,我这次更改请求完成了,轮到下一个了
                sema = tmp->dow_sema;
                tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next;
                _dispatch_thread_semaphore_signal(sema);
            }
        } else {
            // 12. 非首次请求,进入这块逻辑块
            dow.dow_sema = _dispatch_get_thread_semaphore();
            for (;;) {
                // 13. 遍历每一个后续请求,如果状态已经是Done,直接进行下一个
                // 同时该状态检测还用于避免在后续wait之前,信号量已经发出(signal)造成
                // 的死锁
                tmp = *vval;
                if (tmp == DISPATCH_ONCE_DONE) {
                    break;
                }
                dispatch_atomic_store_barrier();
                // 14. 如果当前dispatch_once执行的block没有结束,那么就将这些
                // 后续请求添加到链表当中
                if (dispatch_atomic_cmpxchg(vval, tmp, &dow)) {
                    dow.dow_next = tmp;
                    _dispatch_thread_semaphore_wait(dow.dow_sema);
                }
            }
            _dispatch_put_thread_semaphore(dow.dow_sema);
        }
    }
    

    参考文章:
    http://blog.csdn.net/fishmai/article/details/52047249

    相关文章

      网友评论

      • jiayoubaobao:源码就在哪里,不偏不倚,请你尊重自己也尊重别人的劳动成果,你的文章我确实拜读过,最后的源码从你哪里粘来的,另已附上你文章的链接,也请读者自己甄别吧。希望大家以后能多分享心得吧,感谢你的留言,也感谢你在这个topic上的见解,但是抄袭...恕我绝不认同...
      • 博哥小时候好厉害:抄袭狗🐶 恶心

        原地址:http://satanwoo.github.io/2016/04/11/dispatch-once/
        水晶屋_Crystal:@博哥小时候好厉害 或许只是自己记录下来供自己以后查看呢 :flushed:

      本文标题:你不知道的dispatch_once

      本文链接:https://www.haomeiwen.com/subject/fwytsttx.html