为什么我用dispatch_once搞死了?

时间:2013-10-04 08:02:08

标签: ios objective-c macos grand-central-dispatch semaphore

为什么我会死锁?

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

        [self foo];

    });

    // whatever...
}

我希望在第一次通话时执行两次foo

2 个答案:

答案 0 :(得分:23)

现有的答案都不是很准确(一个是错误的,另一个是有点误导,错过了一些关键细节)。首先,让我们去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是使后续调用者(在该过程的剩余生命周期内)检查已完成状态的便宜之处。

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

  • 第一次调用-foo时,onceToken为nil(由于静态保证初始化为0而得到保证),并且原子地更改为链接列表的头部服务员。
  • 当你从块内部递归地调用-foo时,你的线程被认为是“第二个调用者”,并且存在于这个新的较低堆栈帧中的服务器结构被添加到列表中然后你去等信号灯。
  • 这里的问题是这个信号量永远不会被发出信号,因为为了让它发出信号,你的块必须完成执行(在更高的堆栈帧中),现在由于死锁而不能发生。 / LI>

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

答案 1 :(得分:2)

您可以稍微更改一下代码,以便调用在块之外并且没有死锁,如下所示:

- (void)foo
{
    static dispatch_once_t onceToken;
    BOOL shouldRunTwice = NO;
    dispatch_once(&onceToken, ^{
        shouldRunTwice = YES;
    });
    if (shouldRunTwice) {
        [self foo];
    }
    // whatever...
}