【翻訳】Concurrency in Swift (Grand Central Dispatch Part 1)

2023/07/18に公開

このパートでは、以下のトピックを取り上げます。

  1. 並行処理の必要性
  2. 並行処理と並列処理とは何か
  3. スレッドに関するいくつかのポイント
  4. グランド・セントラル・ディスパッチとは
  5. GCDの有無による並行処理・並列処理の方法
  6. GCDを使わない場合の開発者の課題とGCDによる解決方法
  7. GCDによる開発者の責任
  8. GCDの利点
  9. ディスパッチキューとその種類
  10. カスタムキューとグローバルキューの違い
  11. キューの同期ディスパッチと非同期ディスパッチ
  12. コード例

なぜ並行処理なのか

あなたがメインスレッドにいて、サーバーからデータが必要だとします。
あなたはサーバーにデータを要求し、サーバーから応答が来るまで待ちます。
この間、メイン・スレッドはUI関連の作業を一切行わないので、アプリケーションは無応答になります。
例えば、サーバーが応答を返すのに10秒かかるとします。
この間、ユーザーがボタンをタップしても応答しないので、ユーザーにとって非常に迷惑です。

この2つのタスクを同時に、あるいはほぼ同時に実行し(コンテキスト・スイッチング)、
1つのスレッドをUI処理だけに専念させ、
もう1つのスレッドは処理に時間のかかるタスクにかかりきりにしたらどうでしょう。
先に進む前に、まずいくつかの概念を理解してください。

同時実行

並行処理とは、アプリケーションがタイムスライシングを使って、
同時に(同時並行的に)複数のタスクを処理することを意味します。
コンピュータにCPUが1つしかない場合、アプリケーションは同時に複数のタスクを処理することはできませんが、
コンテキスト・スイッチングと呼ばれる技術を使って、
アプリケーション内部で一度に複数のタスクが処理されます。
次のタスクを開始する前に、1つのタスクが完全に終了することはありません。

今回取り上げたケースで、同時実行を使用してネットワーク・コールを実行する場合、
コンテキスト・スイッチングを使用して命令を実行するメインとバックグラウンドの2つのスレッドが
存在することになります。プロセッサがネットワーク・コールを実行している間、
メイン・スレッドは何のタスクも実行しません。

コンピューティングにおいてコンテキスト・スイッチとは、プロセスやスレッドの状態を保存しておき、
後で同じ時点から復元して実行を再開できるようにすることです。
これにより、複数のプロセスが1つのCPUを共有できるようになり、
マルチタスク・オペレーティング・システムの不可欠な機能となっています。

並列性

並列性とは、複数のことが同時に起こる(コンテキストの切り替えがない)という概念です。

今回取り上げたケースでは、ネットワークコールを並列に実行すると、
メインとバックグラウンドの2つのスレッドが2つの異なるコアで命令を実行することになり、
従来に比べて非常に高速になるが、物理的な要件が増えます。

下図のように、並列処理では2つのスレッドが継続的にタスクを実行しますが、
並行処理ではスレッド1がタスクを実行しているときはスレッド2はアイドル状態です。

スレッドに関するいくつかの概念

  1. シングルコアで10個のスレッドを作成すると、
    同時実行/タイムスライス/コンテキストスイッチを使って同じコアで実行されます。

  2. 10コアのマシンで10個のスレッドを作成した場合、
    すべてのスレッドが1つのコアでコンテキスト・スイッチングを使って実行される可能性があります。

  3. 1コアのマシンに1000個のスレッドを作成した場合、
    コンテキスト・スイッチングしか行われないので、最適なスレッド数を作成するのは難しいです。

グランド・セントラル・ディスパッチ (GCD)

グランド・セントラル・ディスパッチ(GCD)は、アプリケーションで並行/並列処理を行うための低レベルAPIです。

GCDはどのように並行処理/並列処理を行うか

フード下のGCDは、共有スレッドプールを管理し、そのプールに最適な数のスレッドを追加します。
GCDを使用して、コードのブロックや作業項目をキューに追加し、
GCDがそれをどのスレッドで実行するかを決定する。
GCDは、システムの物理的な状態や現在の負荷に応じて、このタスクを同時または並列に実行します。

注意:GCDに2つのタスクを渡した場合、同時実行か並列実行かは分かりません。

これ以降、同時実行(Concurrency / Parallelism)という用語を使用します。

GCDなし

以前は、手動でスレッドを作成して並行処理を行なっていました。
あるコアにスレッドを作成し、タスクを実行させます。
スレッド・ソリューションは低レベルのソリューションであり、手動で管理しなければなりません。

GCDなしの開発者の課題

  1. アプリケーションの最適なスレッド数を決めるのは開発者の責任です。何千ものスレッドを作成すると、
          ほとんどの場合、実際の作業の代わりにコンテキスト・スイッチングが行われるからです。
          アプリケーションの最適なスレッド数は、現在のシステム負荷に基づいて動的に変更できます。

  2. スレッドで一般的に使われる同期メカニズムによって複雑さが増します。

  3. 余分なコアをより効果的に利用するのは、開発者やアプリケーションの責任です。

  4. 効率的に使用できるコア数、これはアプリケーションが独自に計算するのは困難なことです。

GCDの解決方法

  1. GCDは、共有スレッドプールを管理し、そのプールに最適な数のスレッドを追加します。

  2. GCDはスレッド管理コードをシステムレベルに移しました。
    なぜなら、システムは単一アプリケーションに比べてコアを効率的に使用できるからです。

GCDによる開発者の責任

あなたがしなければならないことは、
同時に実行したいタスクを定義し、適切なディスパッチキューに追加することだけです。
GCDは、必要なスレッドの作成と、それらのスレッド上で実行されるタスクのスケジューリングを行います。

GCDの利点

  1. シンプルなプログラミングインターフェースを提供します。(Swift)
  2. 自動スレッドプール管理
  3. 手動でスレッドを作成・管理するのに比べ、高速性を提供
  4. カーネルに負荷をかけない
  5. 現在のシステム負荷に基づく動的なスレッドスケーリング
  6. スレッドスタックがアプリケーションメモリになく、システムメモリにあるため、メモリ効率が良い
  7. これらのロジックがシステムレベルに移動するため、コアを効率的に使用できる

ディスパッチ・キュー

ディスパッチ・キューは、カスタム・タスクを実行するためのCベースのメカニズムです。
ディスパッチ・キューは常に、キューに追加されたのと同じ順番でタスクのデキューを行い、開始する)。
ディスパッチ・キューはスレッドセーフであり、複数のスレッドから同時にアクセスです。
ディスパッチ・キューはスレッドではありません。

もし、GCDを通して並行タスクを実行したい場合、適切なディスパッチキューに追加します。
GCDはタスクを選択し、ディスパッチキューで行われた設定に基づいてそれらを実行します。

ディスパッチ・キューはGCDの中核です。
ディスパッチキューの設定に基づいて、GCDは同時実行タスクを選択し、実行します。

シリアルディスパッチキュー

  1. シリアルディスパッチキュー(プライベートディスパッチキューとも呼ばれる)

  2. キューに追加された順番に、一度に一つのタスクを実行します。
    例えば、5つのタスクをシリアル・ディスパッチ・キューに追加した場合、GCDは最初のタスクから開始し、
    最初のタスクが完了するまで実行し、2番目のタスクは選択しません。

  3. シリアルキューは、特定のリソースへのアクセスを同期させるためによく使われます。
          例えば、2つのネットワーク呼び出しがあり、両方とも10秒かかるので、
    この2つのタスクをいくつかのバックグラウンドスレッドに移動させることにしたとします。

  4. シリアル・キューは一度に1つのスレッドしか使用しないが、同じスレッドで実行することは保証されていない。

  5. 必要な数だけシリアル・キューを作成することができ、
    各キューは他のすべてのキューに対して並行して動作します。
    (各キューは他のすべてのキューに対して同時に動作します。)
    言い換えると、4つのシリアル・キューを作成した場合、各キューは一度に1つのタスクしか実行しませんが、
      それでも最大4つのタスクが各キューから1つずつ同時に実行されます。

  6. 同じ共有リソースにアクセスする2つのタスクがあるが、異なるスレッドで実行されている場合、
          どちらかのスレッドが先にリソースを変更する可能性があり、
          両方のタスクが同時にリソースを変更しないようにロックを使用する必要があります。
    ディスパッチ・キューを使用すると、両方のタスクをシリアル・ディスパッチ・キューに追加して、
          いつでも1つのタスクだけがリソースを変更できるようにすることができます。
          ディスパッチ・キューは主にアプリケーションのプロセス空間で動作し、
          絶対に必要な場合にのみカーネルを呼び出します。

コンカレント・ディスパッチ・キュー

  1. 同時実行キュー(グローバル・ディスパッチ・キューの一種としても知られている)は、
          1つ以上のタスクを同時に実行します。

  2. このグローバル・キューに4つの別々のタスクを追加した場合、それらのブロックはキューに追加されたのと
          同じ順番でタスクを開始します。GCDは最初のタスクを選び、そのタスクを実行し、最初のタスクの完了を
          待たずに2番目のタスクを開始します。これは、バックグラウンド実行だけでなく、これらのブロックが
          他のディスパッチされたブロックと同時に実行されても構わないという場合に理想的です。

  3. 現在実行中のタスクは、ディスパッチキューが管理する個別のスレッド上で実行されます。

  4. 任意の時点で実行されているタスクの正確な数は可変であり、システムの状況に依存します。
          GCDがこれらのタスクを実行するためにいくつのスレッドを使用するかは、システムの状態によって異なり、
          1つのスレッドを使用する可能性もあれば、4つのスレッドを使用する可能性もあります。

  5. GCDでは、カスタム並行キューを作成するか、グローバル並行キューを使用するか、
          2つの方法でブロックを並行実行することができます。後で多くの実験を行う予定です。

カスタムキューとグローバルキューの違い

図1に示すように、我々は2つの同時実行キューを作成しました。
グローバルキューはシステム全体で共有される同時実行キューであるため、常に同じキューを返すのに対し、
カスタム同時実行キューは作成するたびに新しいキューを返すプライベートなキューであることが分かります。

異なる優先度を持つ4つのグローバル並行キューが存在します。グローバル並行キューを設定する際、
優先度を直接指定することはありません。その代わりに、QoS(Quality of Service)を指定します。
QoSには、User-interactive、User-initiated、Utility、Backgroundがあり、
User-interactiveの優先度が最も高く、Backgroundの優先度が最も低くなります。
どのような場合に使用するかは、次のリンクから確認できます。
https://medium.com/@crafttang/ios-grand-central-dispatch-cf9b08d9796b


図1

グローバルキューと比較して、カスタムキューを使用することで実行できるタスクは以下のとおりです。

・ カスタムキューでデバッグするために意味のあるラベルを指定することができます。
・ キューの一時停止
・ バリアタスクの投入

メインディスパッチキュー

  1. メイン・ディスパッチ・キューは、アプリケーションのメイン・スレッド上でタスクを実行する、
          グローバルに利用可能なシリアル・キューです。

  2. このキューは、キューイングされたタスクの実行と、ランループに接続された他のイベントソースの実行を
          インターリーブするために、アプリケーションのランループ(存在する場合)と共に動作します。
          このキューはアプリケーションのメインスレッド上で実行されるため、
         メインキューはしばしばアプリケーションの重要な同期ポイントとして使用されます。

同期と非同期

GCDでは、キューを同期または非同期にディスパッチすることができます。

一般に、同期関数は、キュー内のすべてのタスクが完了し、キューが空になるまで、
キュー同期をディスパッチすると、タスクが完了した後、呼び出し元に制御を返します。

一方、非同期関数は、キューを非同期でディスパッチすると、
タスクが完了する前に呼び出し元に即座に制御を返します。

図2に示すように、同時実行グローバル・キューで時間のかかるタスクを実行しても、
メイン・スレッドmainで同時実行グローバル・キュー同期をディスパッチしているため、
メイン・スレッドはビジー状態のままであり、そのキュー上のすべてのタスクが実行されるまで待機します。
これらの命令は、図3に示すように連続的に実行されます。


図2


図3

図4に示すように、キューを非同期にディスパッチすると、すぐにメイン・スレッドに戻り、
メイン・スレッドが最初に印刷を行います。また、これらはグローバルな同時実行キューなので、
これらのキュー上のタスクは同時に実行されます。
いくつかのユースケースを入れてみましょう。

  1. コンパイラが最初のグローバルキューを実行すると、非同期なのですぐにメインスレッドに制御が戻り、
          GCDがスレッドを取得して、そのスレッド上で最初のグローバルキュータスクを並行して実行します。

  2. コンパイラは非同期で第二グローバルキューを実行し、メインスレッドに直ちに制御を戻し、
          GCDはスレッドを取得してそのスレッド上で第二グローバルキュータスクを同時に実行します。

  3. コンパイラは、メインスレッドにあった出力命令をシリアルに実行します。


図4

図5.1に示すように、デッドロックが発生した。外側ブロックは内側ブロックの完了を待っており、
外側ブロックが完了する前に内側ブロックが開始されることはありません。

並行コンピューティングにおいてデッドロックとは、グループの各メンバーが、
自分自身を含む他のメンバーのアクションを待っている状態のことです。


図5.1

appleドキュメントより "重要: dispatch_sync関数やdispatch_sync_f関数を、
関数に渡す予定のキューと同じキューで実行されているタスクから呼び出すべきではありません。
これは、デッドロックが保証されている直列キューでは特に重要ですが、並列キューでも避けるべきです。"

図5.2に示すように、シリアルキューであるメインキュー上でsyncを呼び出しているため、
これもデッドロックを引き起こします。


図5.2

図6に示すように、プライベート/カスタムシリアルキューを作成し、キューがシリアルであるため、
そこに2つのタスクを追加しました。

注:メイン・スレッドをブロックしないように、キューを非同期でディスパッチしています。


図6

図7に示すように、プライベート/カスタム並行キューを作成し、2つのタスクを追加しました。

注:メイン・スレッドをブロックしないように、キューを非同期にディスパッチしています。


図7

上述したように、4つのシリアルキューを作成した場合、各キューは一度に1つのタスクしか実行しませんが、
図8に示すように、各キューから1つずつ、最大4つのタスクを同時に実行することができます。


図8

DispatchWorkItem

DispatchWorkItemは、タスクをディスパッチキューに保存し、後で使用するために使用されます。
そして、そのタスクに対していくつかのオペレーションを実行することができ、
コードの後半で不要になれば、タスクをキャンセルすることもできます。

以下のコードに示すように、まずキューを作成し、実行を待つ1行のコードで
DispatchWorkItemオブジェクトを作成しました。

次に、ワークアイテムをキャンセルする前に2つのタスクを作成し、
ワークアイテムをキャンセルした後に別のタスクを作成しました。

しかし、出力を見ると、実行されているのは1つのタスクだけで、
残りはタスク2が1秒後に実行されることになっていたのでキャンセルされ、
タスク3はワークアイテムをキャンセルした後に初期化されました。

let queue = DispatchQueue(label: "com.swiftpal.dispatch.workItem")

//  Create a work item
let workItem = DispatchWorkItem() {
    print("Stored Task")
}

// Task 1
queue.async(execute: workItem)

// Task 2
queue.asyncAfter(deadline: DispatchTime.now() + 1, execute: workItem)

// Work Item Cancel
workItem.cancel()

// Task 3
queue.async(execute: workItem)

if item.isCancelled {
    print("Task was cancelled")
}

/* Output:
 Stored Task
 Task was cancelled
 */

次回予告

次のパートでは、他のGCDコンポーネントを取り上げます。

有益なリンク

https://www.kodeco.com/28540615-grand-central-dispatch-tutorial-for-swift-5-part-1-2

https://medium.com/@crafttang/ios-grand-central-dispatch-cf9b08d9796b

https://medium.com/@ellstang/sync-queue-vs-async-queue-in-ios-c607c201ba45

https://medium.com/swift-india/parallel-programming-with-swift-part-1-4-df7caac564ae

https://www.swiftpal.io/articles/what-is-dispatchworkitem-in-gcd-grand-central-dispatch-swift

【翻訳元の記事】

Concurrency in Swift (Grand Central Dispatch Part 1)
https://ali-akhtar.medium.com/concurrency-in-swift-grand-central-dispatch-part-1-945ff05e8863

Discussion