为什么我用dispatch_once搞死了? [英] Why am I getting deadlock with dispatch_once?

查看:145
本文介绍了为什么我用dispatch_once搞死了?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

为什么我会死锁?

- (void)foo
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        [self foo];

    });

    // whatever...
}

我期待 foo 在第一次通话时执行两次。

I expect foo to be executed twice on first call.

推荐答案

两者都没有现有答案非常准确(一个是错误的,另一个是有点误导,错过了一些关键细节)。首先,让我们去权利来源

Neither of the existing answers are quite accurate (one is dead wrong, the other is a bit misleading and misses some critical details). First, let's go right to the source:

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;
    struct _dispatch_once_waiter_s dow = { NULL, 0 };
    struct _dispatch_once_waiter_s *tail, *tmp;
    _dispatch_thread_semaphore_t sema;

    if (dispatch_atomic_cmpxchg(vval, NULL, &dow)) {
        dispatch_atomic_acquire_barrier();
        _dispatch_client_callout(ctxt, func);

        dispatch_atomic_maximally_synchronizing_barrier();
        //dispatch_atomic_release_barrier(); // assumed contained in above
        tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE);
        tail = &dow;
        while (tail != tmp) {
            while (!tmp->dow_next) {
                _dispatch_hardware_pause();
            }
            sema = tmp->dow_sema;
            tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next;
            _dispatch_thread_semaphore_signal(sema);
        }
    } else {
        dow.dow_sema = _dispatch_get_thread_semaphore();
        for (;;) {
            tmp = *vval;
            if (tmp == DISPATCH_ONCE_DONE) {
                break;
            }
            dispatch_atomic_store_barrier();
            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);
    }
}

所以真正发生的是,与其他答案相反, onceToken 从其初始状态 NULL 更改为指向第一个调用者的堆栈上的地址& dow (调用此调用者1)。在块调用之前发生。如果在块完成之前有更多的呼叫者到达,则会将它们添加到服务员的链接列表中,其中的头部包含在 onceToken 中,直到块完成(称为呼叫者2) ...... N)。在被添加到该列表之后,呼叫者2..N在信号量上等待呼叫者1以完成该块的执行,此时呼叫者1将走向链路列表,用信号通知该信号量为每个呼叫者2..N。在步行开始时, onceToken 再次更改 DISPATCH_ONCE_DONE (这是方便地定义为永远不会是有效指针的值,因此永远不会成为被阻止的调用者的链接列表的头部。)将其更改为 DISPATCH_ONCE_DONE 是什么使得它对后续调用者来说很便宜(在过程的剩余生命周期内)来检查已完成的状态。

So what really happens is, contrary to the other answers, the onceToken is changed from its initial state of NULL to point to an address on the stack of the first caller &dow (call this caller 1). This happens before the block is called. If more callers arrive before the block is completed, they get added to a linked list of waiters, the head of which is contained in onceToken until the block completes (call them callers 2..N). After being added to this list, callers 2..N wait on a semaphore for caller 1 to complete execution of the block, at which point caller 1 will walk the linked list signaling the semaphore once for each caller 2..N. At the beginning of that walk, onceToken is changed again to be DISPATCH_ONCE_DONE (which is conveniently defined to be a value that could never be a valid pointer, and therefore could never be the head of a linked list of blocked callers.) Changing it to DISPATCH_ONCE_DONE is what makes it cheap for subsequent callers (for the rest of the lifetime of the process) to check the completed state.

所以在你的情况下,发生了什么:

So in your case, what's happening is this:


  • 第一次拨打 -foo onceToken 是nil(由于静态保证被初始化为0而得到保证),并且原子地改变成为服务员链接列表的头部。

  • 当你从块内部递归调用 -foo ,你的线程被认为是第二个调用者,并且存在于这个新的较低堆栈帧中的服务器结构是添加到列表和你去等待信号量。

  • 这里的问题是这个信号量永远不会被发出信号,因为为了让它发出信号,你的块必须完成执行(在更高的堆栈帧),现在因死锁而无法发生。

  • The first time you call -foo, onceToken is nil (which is guaranteed by virtue of statics being guaranteed to be initialized to 0), and gets atomically changed to become the head of the linked list of waiters.
  • When you call -foo recursively from inside the block, your thread is considered to be "a second caller" and a waiter structure, which exists in this new, lower stack frame, is added to the list and then you go to wait on the semaphore.
  • The problem here is that this semaphore will never be signaled because in order for it to be signaled, your block would have to finish executing (in the higher stack frame), which now can't happen due to a deadlock.

所以,简而言之,是的,你已陷入僵局,这里的实际内容是,不要试图递归调用 dispatch_once 块。但问题肯定是 NOT 无限递归,并且在块完成执行后,该标志绝对不会更改 - 在之前更改 em>块执行完全它如何知道让呼叫者2..N等待呼叫者1完成。

So, in short, yes, you're deadlocked, and the practical takeaway here is, "don't try to call recursively into a dispatch_once block." But the problem is most definitely NOT "infinite recursion", and the flag is most definitely not only changed after the block completes execution -- changing it before the block executes is exactly how it knows to make callers 2..N wait for caller 1 to finish.

这篇关于为什么我用dispatch_once搞死了?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆