paint-brush
グランドセントラルディスパッチ、永遠に@kfamyn
新しい歴史

グランドセントラルディスパッチ、永遠に

Kiryl Famin20m2025/02/28
Read on Terminal Reader

長すぎる; 読むには

この記事では、Swift の Grand Central Dispatch (GCD) を詳しく説明し、スレッド、キュー、コード ブロック、同期実行と非同期実行、QoS、デッドロックの問題などについて説明します。また、実践的な演習を使用して理解を深めます。
featured image - グランドセントラルディスパッチ、永遠に
Kiryl Famin HackerNoon profile picture
0-item
1-item

こんにちは!私の名前はKiryl Faminです。iOS開発者です。


この記事では、 Grand Central Dispatch (GCD)についてまとめて説明します。Swift Modern Concurrency が存在する現在では GCD は時代遅れのように思われるかもしれませんが、このフレームワークを使用するコードは、本番環境でもインタビューでも今後何年も登場し続けるでしょう。


今日は、GCD の基本的な理解にのみ焦点を当てます。キューとスレッドの関係など、マルチスレッドの重要な側面のみを詳細に検討します。これは、他の多くの記事では見落とされがちなトピックです。これらの概念を念頭に置くと、 DispatchGroupDispatchBarrier 、セマフォ、ミューテックスなどのトピックを理解しやすくなります。


この記事は、初心者と経験豊富な開発者の両方に役立つでしょう。技術用語を多用せず、すべてをわかりやすい言葉で説明できるよう努めます。

コンテンツの概要

  • 基本概念: スレッド、マルチスレッド、GCD、タスク、キュー
  • キューの種類: メイン、グローバル、カスタム
  • キューの優先順位: サービス品質 (QoS)
  • シリアルキューと同時キュー
  • タスクを実行する方法: 非同期、同期
  • デッドロック
  • GCD演習
  • リンク

基本概念: スレッド、マルチスレッド、GCD、タスク、キュー

スレッド– 基本的には、一連のシステム命令が配置され実行されるコンテナです。実際、すべての実行可能コードは何らかのスレッド上で実行されます。メイン スレッドとワーカー スレッドを区別します。

アプリケーション内のスレッド


マルチスレッド– システムが複数のスレッドを同時に実行する機能。これにより、複数のコード ブランチを並行して実行できます。


Grand Central Dispatch (GCD) – スレッドの操作を容易にするフレームワーク (マルチスレッドの利点を活用)。主なプリミティブはタスクとキューです。


したがって、GCD は、並行して実行されるコードを簡単に記述できるツールです。簡単な例としては、メイン スレッドの UI 更新を妨げないように、重い計算を別のスレッドにオフロードすることが挙げられます。


タスク– 開発者によってグループ化された一連の命令。どのコードが特定のタスクに属するかは開発者が決定することを理解することが重要です。

例えば:


 print(“GCD”) // a task


 let database = Database() let person = Person(age: 23) // also a task database.store(person)


キュー– GCD の基本的なプリミティブであり、開発者が実行するタスクを配置する場所です。キューは、スレッド間でタスクを分散する役割を担います (各キューはシステムのスレッド プールにアクセスできます)。


基本的に、キューを使用すると、スレッドを直接管理するのではなく、コードをタスクに整理することに集中できます。タスクをキューにディスパッチすると、利用可能なスレッドで実行されます。多くの場合、このスレッドはタスクのディスパッチに使用されたスレッドとは異なります。


列


すべてのGIFのmp4バージョンが見つかりますここまたは、下の「リンク」セクションをご覧ください。

キューの種類

  1. メイン キュー– メイン スレッドでのみ実行されるキュー。シリアルです (詳細は後述)。

     let mainQueue = DispatchQueue.main


  2. グローバル キュー – システムによって提供される 5 つのキュー (優先度レベルごとに 1 つ) があります。これらは同時実行可能です。

     let globalQueue = DispatchQueue.global()


  3. カスタム キュー – 開発者が作成したキュー。開発者は、5 つの優先度とタイプ (シリアルまたは同時実行) のいずれかを選択します (デフォルトではシリアル)。

     let userQueue = DispatchQueue(label: “com.kirylfamin.concurrent”, attributes: .concurrent).

キューの優先順位

サービス品質 (QoS) – キューの優先順位のシステム。タスクがキューに入れられるキューの優先順位が高いほど、より多くのリソースが割り当てられます。QoS レベルは全部で 5 つあります。


  1. .userInteractive – 最高の優先度。即時実行が必要で、メインスレッドで実行するのに適さないタスクに使用されます。たとえば、リアルタイムの画像レタッチを可能にするアプリでは、レタッチ結果を即座に計算する必要があります。ただし、メインスレッドで実行すると、常にメインスレッドで発生する UI の更新とジェスチャ処理 (たとえば、ユーザーがレタッチする領域に指をスライドすると、アプリは「指の下」に結果を即座に表示する必要があります) に干渉します。したがって、メインスレッドに負担をかけずに、できるだけ早く結果を取得します。


  2. .userInitiated – 高速なフィードバックを必要とするタスクの優先度ですが、対話型タスクほど重要ではありません。通常、タスクがすぐに完了せず、待機する必要があることをユーザーが理解しているタスク (サーバー要求など) に使用されます。


  3. .default – 標準の優先度。開発者がキューの作成時に QoS を指定しなかった場合、つまりタスクに特定の要件がなく、コンテキストから優先度を決定できない場合に割り当てられます (たとえば、.userInitiated 優先度のキューからタスクを呼び出すと、タスクはその優先度を継承します)。


  4. .utility – ユーザーからの即時のフィードバックは必要ないが、アプリの動作に必要なタスクの優先度。たとえば、サーバーとのデータの同期や、自動保存のディスクへの書き込みなどです。


  5. .background – 最も低い優先度。例としては、キャッシュのクリーニングがあります。


すべてのキューはシリアルキューまたはコンカレントキューに分類されます


  • シリアル キュー– 名前が示すように、タスクが次々に実行されるキューです。つまり、現在のタスクが終了した後にのみ次のタスクが開始されます。


  • 同時実行キュー– これらのキューを使用すると、タスクを並行して実行できます。つまり、以前のタスクが完了したかどうかに関係なく、リソースが割り当てられるとすぐに新しいタスクが開始されます。開始順序のみが保証され (先にキューに入れられたタスクは後のタスクよりも先に開始されます)、完了順序は保証されないことに注意してください。

    シリアルキューと同時キュー


タスクの実行方法

ここで注意すべき重要な点は、呼び出しスレッドに関連するタスクの実行方法について説明していることです。つまり、タスクを呼び出す方法によって、タスクをキューにディスパッチするスレッド内でイベントがどのように展開されるかが決まります。

非同期( async )

非同期呼び出しとは、呼び出しスレッドがブロックされない呼び出しです。つまり、キューに登録されたタスクの実行を待機しません。

 DispatchQueue.main.async { print(“A”) } print(“B”)


この例では、メインスレッドからメインキューprint("A")タスクを非同期的にエンキューします (このコードは特定のキュー内にはないため、デフォルトでメイン スレッドで実行されます)。したがって、メインスレッドのタスクを待たずに、すぐに実行を続行します。この特定の例では、 print("A")タスクがメインキューにエンキューされ、その後すぐにprint("B")がメインスレッドで実行されます。メインスレッドは現在のコードの実行でビジー状態であるため (メイン キューのタスクはメイン スレッドでのみ実行できます)、現在のタスクprint("B")最初に終了し、メインスレッドが解放された後にのみ、メインキューにエンキューされたタスクprint("A")が実行されます。出力は BA です。


 DispatchQueue.global().async { updateData() DispatchQueue.main.async { updateInterface() } Logger.log(.success) } indicateLoading()


  1. メイン スレッドからデフォルトの優先度でタスクをグローバル キューに非同期的に追加するので、呼び出し元のスレッドはすぐに続行され、 indicateLoading()を呼び出します。


  2. しばらくすると、システムはタスクにリソースを割り当て、スレッド プールの空きワーカー スレッドでタスクを実行し、 updateData()が呼び出されます。


  3. updateInterface()を含むタスクは、メイン キューに非同期的にエンキューされます。呼び出し元のワーカー スレッドは、タスクの完了を待たずに続行します。


  4. タスクは非同期にキューに入れられるため、リソースがいつ割り当てられるかはわかりません。この場合、 updateInterface() (メイン スレッド) とLogger.log(.success) (ワーカー スレッド) のどちらが先に実行されるかは確実にはわかりません (ステップ 1 ~ 2 でもわかりません。メイン スレッドのindicateLoading()とワーカー スレッドのupdateData()のどちらが先に実行されるかはわかりません)。メイン スレッドは UI の更新、ジェスチャ処理、その他の基礎タスクの処理で忙しいですが、それでも常に最大限のシステム リソースを受け取ります。一方、ワーカー スレッドでの実行用のリソースもほぼ即時に割り当てられます。



非同期呼び出し


このアニメーションでは、グローバルキューが空きワーカースレッドでタスクを実行していることに注意してください。

同期してsync

同期呼び出しとは、呼び出しスレッドが停止し、キューに登録されたタスクが完了するまで待機する呼び出しです。

 let userQueue = DispatchQueue(label: "com.kirylfamin.serial") DispatchQueue.global().async { var account = BankAccount() userQueue.sync { account.balance += 10 } let balance = account.balance print(balance) }


ここでは、グローバル キューでタスクを実行しているワーカースレッドから、カスタム キューにタスクを同期的にエンキューして残高を増やします。現在のスレッドはブロックされ、エンキューされたタスクが終了するまで待機します。したがって、カスタム キューのタスクが増分を完了した後にのみ残高が印刷されます。



同期タスク


注: 上記のアニメーションでは、カスタムキューが空きワーカースレッドでタスクを実行します。

デッドロック

同期タスクのコンテキストでは、デッドロック (1 つまたは複数のスレッドが自分自身または互いの進行を無期限に待機する状態) について説明することが重要です。最も一般的な例は、メインスレッドから DispatchQueue.main.sync {} を呼び出すことです。


メインスレッドは現在のタスクの実行でビジー状態です。このタスク内で、何らかのコードを同期的に実行したいと考えています。そのため、同期呼び出しはメインスレッドをブロックします。タスクはメインキューに登録されますが、メインスレッドが現在のタスクの終了を待機してブロックされているため開始できません。また、メインキューのタスクはメインスレッドでのみ実行できます。最初はイメージしにくいかもしれませんが、重要なのは、 DispatchQueue.main.syncでキューに登録されたタスクが現在のタスクの一部になり、現在のタスクの後にキューに登録されることを理解することです。その結果、スレッドは現在のタスクによって占有されているため開始できない現在のタスクの一部を待機します。


 func printing() { print(“A”) DispatchQueue.main.sync { print(“B”) } print(“C”) } 

デッドロック


メイン スレッドがブロックされているため、メイン キューからのprint("B")実行できないことに注意してください

GCD 演習

このセクションでは、これまでに習得したすべての知識を活用して、面接で遭遇する単純なコード ブロックから、並行プログラミングの理解を深める高度な課題まで、さまざまな複雑さの演習について説明します。これらすべてのタスクで問われるのは、「コンソールに何が出力されるか」です。


メイン キューはシリアルで、global() キューは同時実行であり、場合によっては特定の属性を持つカスタム キューが問題に含まれる可能性があることに留意してください。

基本的な練習

まず、通常の難易度のタスク、つまり出力に不確実性が生じる可能性が低いタスクから始めます。これらのタスクは面接で最も多く出題されるタスクです。重要なのは、時間をかけて問題を注意深く分析することです。

すべての演習の完全なコードはここにあります。


タスク 1

 print(“A”) DispatchQueue.main.async { print(“B”) } print(“C”)


  1. メインスレッドでは、 print("A")が実行されます。
  2. print("B")のタスクは、メイン キューに非同期でエンキューされます。メイン スレッドがビジー状態であるため、このタスクはキュー内で待機します。
  3. メインスレッドでは、 print("C")が実行されます。
  4. メイン スレッドが空いているとき (前のタスクが完了した後、UI の更新、ジェスチャの処理など、メイン キューのタスクだけでなく、メイン スレッドで処理する必要がある他のイベントがある可能性があります。より深く理解するには、 RunLoopの詳細を参照してください)、キューに入れられたタスクprint("B")が実行されます。


答え:ACB


タスク 2

 print(“A”) DispatchQueue.main.async { print(“B”) } DispatchQueue.main.async { print(“C”) } print(“D”)


  1. メインスレッドでは、 print("A")が実行されます。
  2. print("B")はメイン キューに登録されます。メイン スレッドが使用可能になるまでメイン キューは続きます。
  3. print("C")のタスクは print("B") の後にキューに入れられ、待機します。
  4. メインスレッドは実行を継続し、「D」を出力します。
  5. メイン スレッドが使用可能になると (他の RunLoop タスクを処理した後)、キューに入れられた最初の操作print("B")が実行されます。
  6. メイン スレッドが再び解放された後 (他の RunLoop タスクを処理した後 - 今後は全体の順序に影響しないためこの詳細は省略します)、 print("C")タスクが実行されます。


答え:ADBC


いくつかの例では説明を少し簡略化し、システムが同期呼び出しの実行を最適化するという事実を省略していることをすぐに述べておきます。これについては後で説明します。


タスク 3

 print(“A”) DispatchQueue.main.async { // 1 print(“B”) DispatchQueue.main.async { print(“C”) } DispatchQueue.global().sync { print(“D”) } DispatchQueue.main.sync { // 2 print(“E”) } } // 3 print(“F”) DispatchQueue.main.async { print(“G”) }


  1. print("A")はメインスレッドで実行されます。
  2. 非同期タスク (ラベル 1~3) は、現在の (メイン) スレッドをブロックせずにメイン キューに登録されます。
  3. メインスレッドは実行を継続し、 "F"を出力します。
  4. print("G")操作は、前のタスク (手順 1 ~ 3) の後にメイン キューに登録されます。
  5. メイン スレッドが解放されると、キューに入れられた最初の操作print("B")の実行が開始されます。
  6. 次に、 print("C")操作がメイン キュー (現在のタスクがまだ実行中で、 print("G")キュー内でそれに続く) に追加されます。これは非同期で追加されるため、実行を待たずにすぐに進みます。
  7. 次に、 print("D")操作がグローバル キューにエンキューされます。この呼び出しは同期的であるため、グローバル キューがそれを実行するまで (使用可能な任意のワーカー スレッドで実行される可能性があります) 待機してから続行します。
  8. 最後に、 print("E")操作がメイン キューに追加されます。この呼び出しは同期的であるため、タスクが完了するまで現在のスレッドをブロックする必要があります。ただし、メイン キューにはすでにタスクがあり、 print("E")操作はそれらのタスクの後に最後に追加されます。したがって、 print("E")を実行する前に、これらの操作を先に実行する必要があります。ただし、メイン スレッドは現在の操作の実行でまだビジー状態であるため、次のキュー操作に進むことができません。現在の操作の後に"G""C"を印刷する操作がなかったとしても、現在の操作 (手順 1 ~ 3) がまだ完了していないため、スレッドは続行できません。
  • 呼び出しが非同期の場合、 print("E") 操作は、 "G""C"を印刷する操作の後にキューに追加されるだけです。


答え:AFBD

代替回答(2番目の呼び出しがasync場合): AFBDGCE


タスク 4

 let serialQueue = DispatchQueue(label: “com.kirylfamin.serial”) serialQueue.async { // 1 print(“A”) serialQueue.sync { print(“B”) } print(“C”) } // 2

  1. タスク (手順 1 ~ 2) は、カスタム シリアル キューに非同期的にエンキューされます (デフォルトでは、 .concurrent属性を使用していないため、キューはシリアルです)。

  2. システムがリソースを割り当てると、実行が開始され、 "A"が印刷されます。
  3. 同じシリアル キュー内に、 print("B")の同期タスクがキューに登録されます。呼び出しは同期であるため、スレッドは実行を待機してブロックされます。
  4. ただし、キューはシリアルであり、外側のタスク 1-2 でまだビジー状態であるため、 print("B")タスクを開始できず、デッドロックが発生します。


答え:A、デッドロック

この例では、メイン キューでもカスタム キューでも、どのシリアル キューでもデッドロックが発生する可能性があることを示しています。


タスク 5

前のタスクのシリアル キューを同時実行キューに置き換えてみましょう。

 DispatchQueue.global().async { // 1 print("A") DispatchQueue.global().sync { print("B") } print("C") } // 2


  1. タスク (手順 1 ~ 2) は、グローバル (同時実行) キューに非同期的にエンキューされます。
  2. リソースが割り当てられると、実行が開始され、 "A"が出力されます。
  3. 同じグローバル キューでprint("B")を実行するための同期呼び出しが行われ、タスクが完了するまで現在のワーカースレッドがブロックされます。
  4. この場合、スレッドがブロックされていても、グローバル キューは同時実行であるため、現在の操作が終了するのを待たずに、別のスレッドで実行するだけで次の操作の実行を開始できます。したがって、呼び出しスレッドは、 print("B")タスクが別のワーカー スレッドで実行されるのを待機します。
  5. タスクが完了すると、最初の呼び出しスレッドがブロック解除され、 "C"が出力されます。

答え:ABC


タスク 6

 print("A") DispatchQueue.main.async { // 1 print("B") DispatchQueue.main.async { // 2 print("C") DispatchQueue.main.async { // 3 print("D") DispatchQueue.main.sync { print("E") } } // 4 } // 5 DispatchQueue.global().sync { // 6 print("F") DispatchQueue.global().sync { print("G") } } // 7 print("H") } // 8 print("I")


  1. メインスレッドは"A"を出力します。

  2. 非同期タスク (手順 1 ~ 8) は、現在のスレッドをブロックせずにメイン キューに登録されます。

  3. メインスレッドは継続し、 "I"を出力します。

  4. その後、メイン スレッドが解放されると、メイン キューに登録されたタスクが実行を開始し、 "B"を出力します。

  5. 別の非同期タスク (手順 2 ~ 5) がメイン キューに登録され、現在のスレッドはブロックされません。

  6. 現在のスレッドで実行を継続し、操作 6 ~ 7 の同期ディスパッチがグローバル キューに行われます。これにより、タスクが完了するまで現在の (メイン) スレッドがブロックされます。

  7. 操作 6 ~ 7 は別のスレッドで実行を開始し、 "F"を出力します。

  8. print("G")操作はグローバル キューに同期的にディスパッチされ、完了するまで現在のワーカー スレッドをブロックします。

  9. "G"が出力され、この操作がディスパッチされたワーカー スレッドがブロック解除されます。

  10. 操作 6 ~ 7 が完了し、ディスパッチ元のスレッド (メイン スレッド) のブロックが解除され、 "H"が出力されます。

  11. 操作 1 ~ 2 が完了すると、実行はメイン キュー内の次の操作 (操作 2 ~ 5) に移動し、開始されて"C"が出力されます。

  12. 操作 3 ~ 4 は、スレッドをブロックせずにメイン キューに登録されます。

  13. 現在の操作 (2–5) が終了すると、次の操作 (3–4) の実行が開始され、 "D"が出力されます。

  14. print("G")操作はメイン キューに同期的にディスパッチされ、現在のスレッドをブロックします。

  15. その後、システムはメイン スレッドでprint("E")操作が実行されるまで無期限に待機します。スレッドがブロックされているため、デッドロックが発生します。


答え:AIBFGHCD、デッドロック

中級レベルの練習

中程度の難易度のタスクには不確実性が伴います。このような問題は面接でもまれに発生します。


タスク 7

 DispatchQueue.global().async { print("A") } DispatchQueue.global().async { print("B") }


  1. print("A")現在のスレッドをブロックすることなく、グローバル キューに非同期的にエンキューされます。
  2. システムがグローバル キューのタスクにリソースを割り当てるのを待ちます。理論上は、これはいつでも発生する可能性があります。print print("B")をキューに入れる次のコマンドを実行する前であってもです。この特定のケースでは、次のタスクが最初にキューに追加され、その後にのみグローバル キューにリソースが割り当てられます。これは、メイン スレッドに最も多くのリソースが割り当てられ、メイン スレッドの次の操作が非常に軽量 (単にタスクを追加する操作) であるためです。実際には、グローバル キューのリソース割り当てよりも高速に実行されます。次のセクションでは、反対のシナリオについて説明します。
  3. print("B")はグローバル キューに登録されます。
  4. 一方、グローバル キューがリソースの割り当てを待機している間、メイン スレッドは続行されます。
  5. リソースが利用可能になると、両方のタスクが実行されます。 "A"を印刷するタスクが"B"よりも早く開始される可能性がありますが、印刷はアトミック操作ではないため (出力がコンソールに表示される瞬間は操作の終わり近くです)、順序を保証することはできません。


答え: (AB)

括弧は、文字が AB または BA の任意の順序で出現する可能性があることを示します。


タスク8

 print("A") DispatchQueue.main.async { print("B") } DispatchQueue.global().async { print("C") }

ここでは、「A」が最初に印刷されることだけが確実です。メイン キューのタスクとグローバル キューのタスクのどちらが速く実行されるかを正確に判断することはできません。


答え:A(BC)


タスク9

 DispatchQueue.global(qos: .userInteractive).async { print(“A”) } DispatchQueue.main.async { // 1 print(“B”) }


そして

DispatchQueue.global(qos: .userInteractive).async { print(“A”) } print(“B”) // 1


一方では、どちらの場合もprint("B")メイン スレッドで実行されます。また、グローバル キューにリソースが割り当てられるタイミングを正確に判断することはできません。そのため、理論的には、メイン スレッドで // 1 とマークされたポイントに到達する直前に"A"が印刷される可能性があります。ただし、実際には、最初のタスクは常に AB として印刷され、2 番目のタスクは BA として印刷されます。これは、最初のケースでは、 print("B")が少なくともメイン スレッドの次の RunLoop 反復 (または数反復後) で実行されるのに対し、2 番目のケースでは、 print("B")メイン スレッドの現在の RunLoop 反復で実行されるようにスケジュールされているためです。ただし、順序は保証できません。


両方のタスクの答え: (AB)


タスク 10

 print("A") DispatchQueue.global().async { print("B") DispatchQueue.global().async { print("C") } print("D") }

出力の始まりが"AB"であることは明らかです。 print("C")をキューに入れた後、リソースがいつ割り当てられるかを正確に判断することはできません。このタスクはprint("D")前または後に実行される可能性があります。これは実際にも時々発生します。


答え:AB(CD)


タスク 11

 let serialQueue = DispatchQueue(label: “com.kirylfamin.serial”, qos: .userInteractive) DispatchQueue.main.async { print(“A”) serialQueue.async { print(“B”) } print(“C”) }

繰り返しになりますが、カスタム キューで print("B") にリソースが割り当てられるタイミングを正確に判断することはできません。実際には、メイン スレッドに最高の優先度が与えられるため、通常は "C" が "B" の前に印刷されますが、これは保証されません。


答え:A(BC)


タスク 12

 DispatchQueue.global().async { print("A") } print("B") sleep(1) print("C")

ここでは、1 秒間のスリープによってグローバル キューにリソースを割り当てるのに十分な時間が確保されるため、出力が BAC になることは明らかです。メイン スレッドがスリープによってブロックされている間 (本番環境では実行しないでください)、 print("A")別のスレッドで実行されます。


答え: BAC


タスク 13

 DispatchQueue.main.async { print("A") } print("B") sleep(1) print("C")

この場合、 print("A")はメイン キューに登録されているため、メイン スレッドでのみ実行できます。ただし、メイン スレッドはコードの実行を継続します ( "B"を印刷し、スリープし、 "C"を印刷します)。その後でのみ、RunLoop はキューに登録されたタスクを実行できます。


答え:BCA

高度なタスク

面接でこれらの問題に遭遇する可能性は低いですが、これらの問題を理解することで GCD をよりよく理解できるようになります。

ここでのCounterクラスは参照セマンティクスのためだけに使用されます。

 final class Counter { var count = 0 }


タスク 14

 let counter = Counter() DispatchQueue.global().async { DispatchQueue.main.async { print(counter.count) } for _ in (0..<100) { // 1 counter.count += 1 } }

ここでは、メイン スレッドのビジー状態に応じて、0 から 100 までの任意の数値が出力されます。ご存知のとおり、非同期タスクがリソースを取得するタイミングを正確に予測することはできません。ワーカー スレッドのループの前、最中、または後に発生する可能性があります。


答え: 0-100


タスク 15

 DispatchQueue.global(qos: .userInitiated).async { print(“A”) } DispatchQueue.global(qos: .userInteractive).async { print(“B”) }

QoS では、優先度の高いキューがリソースをより速く受け取ることが保証されませんが、iOS はそうしようとします。実際には、ここでの出力は (AB) です。


答え: (AB)


タスク 16

 var count = 0 DispatchQueue.global(qos: . userInitiated).async { for _ in 0..<1000 { count += 1 } print(“A”) } DispatchQueue.global(qos: .userInteractive).async { for _ in 0..<1000 { count += 1 } print(“B”) }

どの実行が最初に開始されるかはわからないため、1000 の操作にわたっても、どのタスクが早く完了するかを判断することはできません。


答え: (AB)


タスク 16.2

操作が同時に実行を開始すると仮定した場合の出力はどうなりますか?


.userInteractive キューにはより多くのリソースが割り当てられているため、1000 操作の範囲で、そのキュー内の実行は常により速く終了します。


答え: BA


タスク 17

同様のアプローチを使用して、前のセクションの不確実性のあるタスク (たとえば、タスク 12) を変更できます。


 let counter = Counter() let serialQueue = DispatchQueue(label: “com.kirylfamin.serial”, qos: .userInteractive) DispatchQueue.main.async { serialQueue.async { print(counter.count) } for _ in 0..<100 { counter.count += 1 } }


0 から 100 までの任意の数値を印刷できます。0 を印刷できるという事実は、タスク 12 では"C"の出力が常に"B"の前に発生することを保証できないことを示しています。基本的に何も変わっていないためです。ループは印刷よりもわずかに多くのリソースを消費するだけです (実行前であってもループを開始するだけでは、実際には完全な不確実性が生じることに注意してください)。

答え: 0-100


タスク 18

 DispatchQueue.global(qos: .userInitiated).async { print(“A”) } print(“B”) DispatchQueue.global(qos: .userInteractive).async { print(“C”) }


ここでも同様の状況が発生します。理論上は、 print("A") print("B")よりも速く実行される可能性があります ( print("B")少し重いものに置き換えた場合)。実際には、 "B"常に最初に印刷されます。ただし、 print("C")をキューに入れる前にprint("B")を実行すると、メイン スレッドでprint("B")に費やされる余分な時間は、多くの場合、.userInitiated キューがリソースを取得して print("A") を実行するのに十分であるため、 "A" "C"より前print("A")される可能性が大幅に高まります。ただし、これは保証されておらず、 "C"方が速く印刷される場合もあります。したがって、理論上は完全に不確実ですが、実際には B(CA) になる傾向があります。


答え: (BCA)


タスク 19

 DispatchQueue.global().sync { print(Thread.current) }


同期状態に関するドキュメント:


「パフォーマンスの最適化のため、この関数は可能な限り現在のスレッドでブロックを実行しますが、1 つの例外があります。メイン ディスパッチ キューに送信されたブロックは常にメイン スレッドで実行されます。」

これは、最適化の目的で、同期呼び出しが、呼び出されたのと同じスレッドで実行される可能性があることを意味します ( main.syncは例外で、これを使用するタスクは常にメイン スレッドで実行されます)。したがって、現在の (メイン) スレッドが出力されます。


回答:メインスレッド


タスク 20

 DispatchQueue.global().sync { // 1 print(“A”) DispatchQueue.main.sync { print(“B”) } print(“C”) }

デッドロックが発生したため、 "A"のみが出力されます。最適化により、タスク (ラベル 1) がメイン スレッドで実行を開始し、 main.syncを呼び出すとデッドロックが発生します。


答え:А、デッドロック


タスク 21

 DispatchQueue.main.async { print("A") DispatchQueue.global().sync { print("B") } print("C") }


最適化により、 print("B")タスクはキューに入れられるのではなく、現在の実行スレッドに「接合」されます。したがって、コードは次のようになります。

 DispatchQueue.global().sync { print("B") }


次と同等になります:

 print(“B”)


答え:ABC


これらのタスクから、main.sync は、呼び出しがメイン スレッドから行われていないことが確実な場合にのみ、非常に慎重に使用する必要があることがわかります。

結論

この記事では、iOS のマルチスレッドの基本概念であるスレッド、タスク、キューとそれらの相互関係に焦点を当てました。GCD がメイン キュー、グローバル キュー、カスタム キュー全体でタスクの実行を管理する方法を調べ、シリアル実行と同時実行の違いについて説明しました。さらに、同期 (sync) タスク ディスパッチと非同期 (async) タスク ディスパッチの重要な違いを調べ、これらのアプローチがコード実行の順序とタイミングにどのように影響するかを強調しました。これらの基本概念を習得することは、応答性の高い安定したアプリケーションを構築し、デッドロックなどのよくある落とし穴を回避するために不可欠です。


この記事で何か役に立つ情報を見つけていただければ幸いです。不明な点があれば、Telegram: @kfamynまでお気軽にご連絡ください。無料でご説明いたします。

関連リンク

  1. すべてのアニメーションを含む YouTube チャンネル - https://www.youtube.com/@kirylfamin
  2. 演習の完全なコード - https://github.com/kfamyn/GCD-Tasks
  3. 私のテレグラム - http://t.me/kfamyn
  4. ランループ - https://developer.apple.com/documentation/foundation/runloop
  5. syncメソッドのドキュメント - https://developer.apple.com/documentation/dispatch/dispatchqueue/sync(execute:)-3segw