【问题标题】:Why am I getting deadlock with dispatch_once?为什么我与 dispatch_once 陷入僵局?
【发布时间】:2013-10-04 08:02:08
【问题描述】:

为什么我会死锁?

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

        [self foo];

    });

    // whatever...
}

我希望foo 在第一次调用时被执行两次。

【问题讨论】:

  • 好像是一种没有任何break条件的递归方法,不要这样做!
  • 为什么你需要调用 foo 两次?
  • 你为什么要称之为递归?!?!?

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


【解决方案1】:

现有的答案都不是很准确(一个大错特错,另一个有点误导并遗漏了一些关键细节)。首先,我们去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 可以使后续调用者(在进程的剩余生命周期中)检查完成状态变得便宜。

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

  • 第一次调用-fooonceToken 为 nil(这是通过保证将静态变量初始化为 0 来保证的),并自动更改为服务员链表的头部。
  • 当您从块内部递归调用-foo 时,您的线程被认为是“第二个调用者”,并且存在于这个新的较低堆栈帧中的等待者结构被添加到列表中,然后您就可以执行了等待信号量。
  • 这里的问题是这个信号量永远不会被发出信号,因为为了发出信号,你的块必须完成执行(在更高的堆栈帧中),现在由于死锁而不能发生。

所以,简而言之,是的,你陷入了僵局,这里的实际要点是,“不要尝试递归调用 dispatch_once 块。”但问题绝对是NOT“无限递归”,并且标志绝对不是在块完成执行后更改——更改它之前 em> 块的执行正是它知道如何让调用者 2..N 等待调用者 1 完成。

【讨论】:

    【解决方案2】:

    你可以稍微修改一下代码,这样调用就在块之外并且没有死锁,像这样:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-04-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-04-28
      • 1970-01-01
      • 2022-07-14
      • 2021-10-12
      相关资源
      最近更新 更多