60

メイン スレッドまたは別のスレッドのいずれかで発生するデリゲート コールバックがあるシナリオに遭遇しましたStoreKit.framework

また、関数が実行される前に発生する必要があるそのコールバックで更新する必要がある UI コードもあったため、最初に考えたのは次のような関数を持つことでした。

-(void) someDelegateCallback:(id) sender
{
    dispatch_sync(dispatch_get_main_queue(), ^{
        // ui update code here
    });

    // code here that depends upon the UI getting updated
}

バックグラウンドスレッドで実行すると、うまく機能します。ただし、メインスレッドで実行すると、プログラムはデッドロックに陥ります。

それだけでも興味深いように思えます。ドキュメントを正しく読んだ場合、ここでdispatch_sync述べたように、実行ループにスケジュールすることを心配せずに、ブロックを完全に実行することを期待します。

最適化として、この関数は可能な場合、現在のスレッドでブロックを呼び出します。

しかし、それは大したことではありません。単純にもう少しタイピングが必要なため、次のアプローチに行き着きました。

-(void) someDelegateCallBack:(id) sender
{
    dispatch_block_t onMain = ^{
        // update UI code here
    };

    if (dispatch_get_current_queue() == dispatch_get_main_queue())
       onMain();
    else
       dispatch_sync(dispatch_get_main_queue(), onMain);
}

ただし、これは少し後ろ向きのようです。これは GCD の作成におけるバグでしたか、それともドキュメントに欠けているものがありますか?

4

6 に答える 6

75

dispatch_sync次の 2 つのことを行います。

  1. ブロックをキューに入れる
  2. ブロックの実行が完了するまで現在のスレッドをブロックします

メイン スレッドがシリアル キューである (つまり、1 つのスレッドのみを使用する) 場合、メイン キューで次のステートメントを実行すると、次のようになります。

dispatch_sync(dispatch_get_main_queue(), ^(){/*...*/});

次のイベントが発生します。

  1. dispatch_syncブロックをメイン キューに入れます。
  2. dispatch_syncブロックの実行が終了するまで、メイン キューのスレッドをブロックします。
  3. dispatch_syncブロックが実行されるはずのスレッドがブロックされているため、永久に待機します。

この問題を理解するための鍵は、dispatch_syncブロックを実行するのではなく、ブロックをキューに入れるだけであるということです。実行ループの将来の反復で実行が行われます。

次のアプローチ:

if (queueA == dispatch_get_current_queue()){
    block();
} else {
    dispatch_sync(queueA, block);
}

まったく問題ありませんが、キューの階層が関係する複雑なシナリオからは保護されないことに注意してください。このような場合、現在のキューは、ブロックを送信しようとして以前にブロックされたキューとは異なる場合があります。例:

dispatch_sync(queueA, ^{
    dispatch_sync(queueB, ^{
        // dispatch_get_current_queue() is B, but A is blocked, 
        // so a dispatch_sync(A,b) will deadlock.
        dispatch_sync(queueA, ^{
            // some task
        });
    });
});

複雑なケースでは、ディスパッチ キューのキー値データを読み書きします。

dispatch_queue_t workerQ = dispatch_queue_create("com.meh.sometask", NULL);
dispatch_queue_t funnelQ = dispatch_queue_create("com.meh.funnel", NULL);
dispatch_set_target_queue(workerQ,funnelQ);

static int kKey;
 
// saves string "funnel" in funnelQ
CFStringRef tag = CFSTR("funnel");
dispatch_queue_set_specific(funnelQ, 
                            &kKey,
                            (void*)tag,
                            (dispatch_function_t)CFRelease);

dispatch_sync(workerQ, ^{
    // is funnelQ in the hierarchy of workerQ?
    CFStringRef tag = dispatch_get_specific(&kKey);
    if (tag){
        dispatch_sync(funnelQ, ^{
            // some task
        });
    } else {
        // some task
    }
});

説明:

  • workerQキューを指すキューを作成しfunnelQます。実際のコードでは、複数の「ワーカー」キューがあり、すべてを一度に再開/一時停止したい場合に便利です (これは、ターゲットfunnelQキューを再開/更新することによって実現されます)。
  • いつでもワーカー キューをファネルする可能性があるため、ファネルされているかどうかを確認するために、funnelQ「ファネル」という単語でタグ付けします。
  • 途中でdispatch_sync何かをしworkerQたり、何らかの理由でしたいのですが、現在のキューdispatch_syncfunnelQのdispatch_syncを避けているので、タグをチェックしてそれに応じて行動します。get は階層を上に移動するため、値は では見つかりませんが、 でworkerQ見つかりfunnelQます。これは、階層内のいずれかのキューが値を格納したキューであるかどうかを調べる方法です。したがって、現在のキューへの dispatch_sync を防ぐためです。

コンテキスト データを読み書きする関数について疑問がある場合は、次の 3 つがあります。

  • dispatch_queue_set_specific: キューに書き込みます。
  • dispatch_queue_get_specific: キューから読み取ります。
  • dispatch_get_specific: 現在のキューから読み取る便利な関数。

キーはポインターによって比較され、逆参照されることはありません。セッターの最後のパラメーターは、キーを解放するデストラクタです。

「あるキューを別のキューに向ける」ことについて疑問に思っているなら、それはまさにそれを意味します。たとえば、キュ​​ー A をメイン キューに向けると、キュー A 内のすべてのブロックがメイン キューで実行されます (通常、これは UI の更新のために行われます)。

于 2013-03-31T02:50:54.313 に答える
53

私はこれをドキュメント(最後の章)で見つけました:

関数呼び出しに渡すのと同じキューで実行されているタスクからdispatch_sync関数を呼び出さないでください。これを行うと、キューがデッドロックします。現在のキューにディスパッチする必要がある場合は、dispatch_async関数を使用して非同期でディスパッチします。

また、私はあなたが提供したリンクをたどり、dispatch_syncの説明でこれを読みました:

この関数を呼び出して現在のキューをターゲットにすると、デッドロックが発生します。

したがって、GCDの問題ではないと思います。唯一の賢明なアプローチは、問題を発見した後に発明したアプローチだと思います。

于 2012-06-12T14:02:19.520 に答える
6

ドキュメントには、現在のキューを渡すとデッドロックが発生することが明確に記載されています。

現在、彼らはなぜそのように設計したのかについては語っていませんが (ただし、実際に機能させるには追加のコードが必要になることを除いて)、このようにする理由は、この特別なケースではブロックが「ジャンプ」するためだと思われます。つまり、通常の場合、ブロックはキュー上の他のすべてのブロックが実行された後に実行されますが、この場合は前に実行されます。

この問題は、GCD を相互排除メカニズムとして使用しようとした場合に発生します。この特定のケースは、再帰的ミューテックスを使用することと同じです。GCD を使用するか、pthreads ミューテックスなどの従来の相互排除 API を使用する方がよいか、または再帰ミューテックスを使用するのが良いかどうかについての議論には入りたくありません。それについては他の人に議論してもらいますが、これに対する需要は確かにあります。特に、扱っているのがメイン キューの場合です。

個人的には、これをサポートするか、別の動作を提供する別の関数があれば、dispatch_sync がより便利になると思います。そう思う人には、Apple にバグレポートを提出するよう強く勧めたい (私が行ったように、ID: 12668073)。

同じことを行う独自の関数を作成することもできますが、これはちょっとしたハックです。

// Like dispatch_sync but works on current queue
static inline void dispatch_synchronized (dispatch_queue_t queue,
                                          dispatch_block_t block)
{
  dispatch_queue_set_specific (queue, queue, (void *)1, NULL);
  if (dispatch_get_specific (queue))
    block ();
  else
    dispatch_sync (queue, block);
}

注: 以前は、dispatch_get_current_queue() を使用した例がありましたが、現在は推奨されていません。

于 2012-11-09T02:08:40.847 に答える
4

dispatch_asyncとperform の両方でdispatch_sync、アクションを目的のキューにプッシュします。アクションはすぐには実行されません。キューの実行ループの将来の繰り返しで発生します。dispatch_asyncとの違いは、アクションが終了するまで現在のキューdispatch_syncをブロックすることです。dispatch_sync

現在のキューで何かを非同期に実行するとどうなるかを考えてみてください。繰り返しますが、すぐには起こりません。それは FIFO キューに置かれ、実行ループの現在の反復が完了するまで待機する必要があります (また、この新しいアクションを実行する前に、キューにあった他のアクションも待機する可能性があります)。

現在のキューで非同期にアクションを実行する場合、将来のある時点まで待機するのではなく、関数を常に直接呼び出さないのはなぜかと疑問に思うかもしれません。答えは、両者には大きな違いがあるということです。多くの場合、アクションを実行する必要がありますが、実行ループの現在の反復で関数がスタックを上って何らかの副作用を実行した後に実行する必要があります。または、実行ループで既にスケジュールされているアニメーション アクションの後にアクションを実行する必要がある場合などです。そのため、多くの場合、コードが表示されます[obj performSelector:selector withObject:foo afterDelay:0](はい、 とは異なります[obj performSelector:selector withObject:foo])。

前に述べたように、dispatch_syncは と同じですがdispatch_async、アクションが完了するまでブロックする点が異なります。したがって、デッドロックが発生する理由は明らかです。ブロックは、少なくとも実行ループの現在の反復が終了するまで実行できません。ただし、続行する前に終了するのを待っています。

理論的にdispatch_syncは、それが現在のスレッドである場合に特別なケースを作成して、すぐに実行することが可能です。performSelector:onThread:withObject:waitUntilDone:(スレッドが現在のスレッドでYES の場合、そのような特殊なケースが存在しwaitUntilDone:ます。) ただし、Apple はキューに関係なく、ここで一貫した動作を行う方がよいと判断したと思います。

于 2012-06-11T19:59:43.503 に答える