web-dev-qa-db-ja.com

現在のキューでdispatch_syncを使用できないのはなぜですか?

メインスレッドまたは別のスレッドのいずれかで発生するデリゲートコールバックがあり、実行時まで(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のドキュメントを読むと、ブロックを実行するだけで、ランループにスケジューリングすることを心配せずに、それが here =:

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

しかし、それはそれほど大したことではなく、単にもう少し入力することを意味し、このアプローチに私を導いた:

-(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の作成のバグでしたか、それともドキュメントに欠けているものがありますか?

57

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

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

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

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

だから、GCDの問題だとは思わない。賢明なアプローチは、問題を発見した後に発明したものだけだと思う​​。

51
lawicko

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に「funnel」という単語をタグ付けします。
  • 私は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の更新のために行われます)。

69
Jano

私はあなたの混乱がどこから来るのか知っています:

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

注意してください、現在のスレッドと言います。

スレッド!=キュー

キューはスレッドを所有せず、スレッドはキューにバインドされていません。スレッドがあり、キューがあります。キューがブロックを実行するたびにスレッドが必要になりますが、それは常に同じスレッドであるとは限りません。スレッドが必要なだけで(毎回異なる場合があります)、ブロックの実行が完了すると(今のところ)、同じスレッドを別のキューで使用できるようになります。

この文が語る最適化は、キューではなくスレッドに関するものです。例えば。 QueueAQueueBの2つのシリアルキューがあるとします。次に、以下を実行します。

_dispatch_async(QueueA, ^{
    someFunctionA(...);
    dispatch_sync(QueueB, ^{
        someFunctionB(...);
    });
});
_

QueueAがブロックを実行すると、一時的に任意のスレッドを所有します。 someFunctionA(...)はそのスレッドで実行されます。同期ディスパッチを実行している間、QueueAは他に何もできません。ディスパッチが完了するまで待機する必要があります。一方、QueueBでは、ブロックを実行してsomeFunctionB(...)を実行するスレッドも必要です。したがって、QueueAはスレッドを一時的に中断し、QueueBは他のスレッドを使用してブロックを実行するか、QueueAはスレッドをQueueBに引き渡します(すべて勝った後)とにかく同期ディスパッチが終了するまでは必要ありません)、QueueBQueueAの現在のスレッドを直接使用します。

言うまでもなく、最後のオプションはスレッドの切り替えが不要なため、はるかに高速です。そして、thisは文が話す最適化です。そのため、異なるキューへのdispatch_sync()が常にスレッドの切り替えを引き起こすとは限りません(異なるキュー、おそらく同じスレッド)。

ただし、dispatch_sync()は、同じキュー(同じスレッド、はい、同じキュー、いいえ)にはまだ発生しません。キューはブロックごとにブロックを実行し、現在ブロックを実行しているとき、現在実行されているブロックが実行されるまで別のキューを実行しないためです。そのため、BlockAを実行し、BlockAは同じキューでBlockBdispatch_sync()を実行します。キューは、BlockBが実行されている限りBlockAを実行しませんが、BlockAが実行されるまでBlockBは実行されません。問題が発生しましたか?これは古典的なデッドロックです。

14
Mecki

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

今、彼らは物事をそのように設計した理由を述べていません(実際にはそれを機能させるために余分なコードが必要になることを除いて)キュー。つまり、通常の場合、キュー上の他のすべてのブロックが実行された後にブロックが実行されますが、この場合は前に実行されます。

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

個人的には、dispatch_syncがこれをサポートしている場合、または代替の動作を提供する別の関数がある場合、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);
}

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

6
Chris Suter

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

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

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

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

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

4
newacct

次のドキュメントから見つかりました。 https://developer.Apple.com/library/ios/documentation/Performance/Reference/GCD_libdispatch_Ref/index.html#//Apple_ref/c/func/dispatch_sync

dispatch_asyncとは異なり、「dispatch_sync」関数はブロックが終了するまで戻りません終わった。この関数を呼び出して現在のキューをターゲットにすると、デッドロックが発生します。

dispatch_asyncとは異なり、ターゲットキューで保持は実行されません。この関数の呼び出しは同期であるため、呼び出し元の参照を「borrows」します。さらに、ブロックで(Block_copyは実行されません。

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

2
arango_86