10

なぜ私は行き詰まっているのですか?

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

        [self foo];

    });

    // whatever...
}

foo最初の呼び出しで 2 回実行されることを期待しています。

4

2 に答える 2

25

既存の回答はどちらもまったく正確ではありません (1 つは完全に間違っており、もう 1 つは少し誤解を招きやすく、重要な詳細を見逃しています)。まず、ソースに行きましょう:

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初期状態から変更されます(この呼び出し元を 1 と呼びます)。これは、ブロックが呼び出される前に発生します。ブロックが完了する前にさらに呼び出し元が到着すると、それらはウェイターのリンクされたリストに追加され、その先頭はブロックが完了するまで保持されます (呼び出し元 2..N と呼びます)。このリストに追加された後、呼び出し元 2..N は、呼び出し元 1 がブロックの実行を完了するまでセマフォで待機します。この時点で、呼び出し元 1 は、呼び出し元 2..N ごとに 1 回セマフォに信号を送るリンクされたリストを歩きます。その散歩の初めに、再びに変更されますNULL&dowonceTokenonceTokenDISPATCH_ONCE_DONEDISPATCH_ONCE_DONE(これは、有効なポインターになることは決してないため、ブロックされた呼び出し元のリンクされたリストの先頭になることは決してない値になるように便利に定義されています)。プロセスの存続期間) 完了状態を確認します。

したがって、あなたの場合、何が起こっているかは次のとおりです。

  • 初めて を呼び出すと-fooonceTokenは nil (0 に初期化されることが保証されている statics によって保証されています) であり、ウェイターのリンクされたリストの先頭になるようにアトミックに変更されます。
  • ブロック内から再帰的に呼び出すと-foo、スレッドは「2 番目の呼び出し元」と見なされ、この新しい下位スタック フレームに存在する待機構造がリストに追加され、セマフォで待機します。
  • ここでの問題は、このセマフォがシグナルされることはないということです。これは、シグナルが送信されるためには、ブロックが (より高いスタック フレームで) 実行を終了する必要があるためです。

要するに、はい、あなたは行き​​詰まっています。ここでの実際的なポイントは、「dispatch_onceブロックを再帰的に呼び出そうとしないこと」です。しかし、問題は間違いなく「無限再帰」ではなく、ブロックの実行が完了した後フラグが変更されるだけではないことは間違いありませ。終わる。

于 2013-10-04T11:24:45.760 に答える
2

次のように、呼び出しがブロックの外側にあり、デッドロックが発生しないように、コードを少し変更できます。

- (void)foo
{
    static dispatch_once_t onceToken;
    BOOL shouldRunTwice = NO;
    dispatch_once(&onceToken, ^{
        shouldRunTwice = YES;
    });
    if (shouldRunTwice) {
        [self foo];
    }
    // whatever...
}
于 2013-12-23T19:03:17.537 に答える