【翻訳】《Swift and Cocoa Essentials》Threads, Queues, and Concurrency

2023/07/25に公開

就職の面接やプロジェクトのミーティングに備えることは、困難であり、圧倒されることさえあります。開発者として精通していなければならない事柄は、驚くほどたくさんあります。あなたがソフトウェアを開発しているプラットフォームの基礎をマスターすることは、常に重要な重点分野であるべきです。そのため、 Cocoacasts で公開されているコンテンツの大部分は、 Swift と Cocoa 開発の基礎に焦点を当てています。

このエピソードは、スレッドと並行処理にズームインします。開発者にとって、これらのテーマは複雑で高度に見えるため、避けることは珍しいことではありません。確かに、スレッドと並行処理は学ぶのが最も簡単な概念ではありませんが、パフォーマンスと信頼性の高いソフトウェアを構築するためには、それらを理解する必要があります。

スレッドとは何か?

多くの開発者が答えを持っていない基本的な質問から始めましょう。スレッドとは何か?スレッドと並行処理に関する低レベルの詳細には立ち入りません。それは間違いなく興味深いですが、あなたが Swift や Cocoa の開発者なら、細かい詳細を理解することはそれほど重要ではありません。

このエピソードがスレッドと並行性の理論的な議論になることは避けたい。もっと具体的に説明しましょう。 Xcode を起動し、iOS セクションから Single View App テンプレートを選択して新しいプロジェクトを作成します。

プロジェクト名を Threads とし、 Create をクリックしてプロジェクトを作成します。

シミュレータまたは物理デバイス上でアプリケーションを実行します。アプリケーションを実行した状態で、下部にあるデバッグ・バーの「一時停止」ボタンをクリックして、アプリケーションのプロセスの実行を一時停止します。

左側のデバッグ・ナビゲーターを開いてください。デバッグ・ナビゲーターには、アプリケーションの実行に使用されるスレッドのリストが表示されます。

書いたコードが実行されない限り、アプリケーションは機能しません。スレッドとは、コマンドが実行されるコンテキストに過ぎません。スレッドとは、我々が作成したアプリケーションを通して実行される行やスレッドのことです。スレッドのスタック・トレースはこれを示しています。スタック・トレースの各フレームは、スレッドによって実行されているコマンドです。スレッドとスタックトレースについては、 Debugging Applications With Xcode (https://cocoacasts.com/series/debugging-applications-with-xcode) で詳しく説明されています。

スレッドは、ユニークな識別子、 stack 、 register のコレクションなど、より多くのプロパティを持っています。これらのプロパティについては、今は心配しないでください。それらは、この議論の残りの部分では重要ではありません。

全ての Cocoa アプリケーションには、最低1つのスレッド、メイン・スレッドがあります。メイン・スレッドは、アプリケーションが動作を開始するスレッドです。デバッグ・ナビゲーターのスレッド一覧の一番上のスレッドがメイン・スレッドです。メイン・スレッドについては、次のエピソードで詳しく説明します。

プロジェクトのシンプルさにもかかわらず、デバッグナビゲータはアプリケーションが複数のスレッドを使用してタスクを実行していることを示しています。2つ以上のスレッドを使用しているので、このアプリケーションはマルチスレッドであると言えます。マルチスレッドは、アプリケーションのパフォーマンスを向上させるためのパターンです。複数のスレッドで作業をスケジューリングすることで、より多くの作業を並行して実行することができます。作業を並行して実行することは、同時実行( concurrency )としても知られています。複数のコマンドを並行または同時に実行します。

マルチスレッドと同時実行は、デバイスがシングルコアのシングルプロセッサから、マルチコアの1つまたは複数のプロセッサに移行したため、年々重要性を増しています。このパワーを活用することは重要ですが、ソフトウェア開発を複雑で困難なものにしているのも事実です。その複雑さを管理するためのオプションはいくつかあります。Grand Central Dispatch はその1つです。

Grand Central Dispatch

スレッドと並行処理は複雑な概念ですが、良いニュースもあります。 Cocoa 開発者として、スレッドと直接やりとりすることはほとんどありません。数年前、 Apple は Grand Central Dispatch 、略して GCD を発表しました。スレッドを手動で作成して管理する必要はほとんどありません。 Grand Central Dispatch は、タスクのスケジューリングを容易にする API を開発者に提供します。どのスレッドがタスクの実行に使われるかは、開発者ではなく Grand Central Dispatch によって処理されます。

先に述べたように、私たちがアプリケーションを開発するデバイスは強力なプロセッサを搭載しており、そのパワーを活用することが重要です。アプリケーションはパフォーマンスと応答性が求められます。これらの目標を達成するのは難しいことです。

Grand Central Dispatch は、ディスパッチ・キューのコレクションを管理します。これらは通常キューと呼ばれます。これらのディスパッチキューに投入された作業は、スレッドのプール上で実行されます。開発者としては、どのスレッドが作業ブロックの実行に使われるかを心配する必要はありませんし、 Grand Central Dispatch はあなたにそれを知られたくありません。また、 Grand Central Dispatch はあなたに知られたくないのです。どのスレッドが特定のタスクに使われるかは、 Grand Central Dispatch の実装の詳細です。 Grand Central Dispatch については後のエピソードで詳しく見ていきましょう。

重要なことは、 Grand Central Dispatch はシステムレベルで動作するということです。つまり、どのプロセスが動いていて、どのリソースが利用可能で、どのように作業をスケジューリングするのが最適かを正確に把握しているということです。これは非常に強力なソリューションです。

キューとは?

Grand Central Dispatch がディスパッチキューのコレクションを管理していることはすでに述べました。その名の通り、ディスパッチキューとは、作業の実行がスケジュールされるキューのことです。

ディスパッチ・キューには、シリアル・ディスパッチ・キューとコンカレント・ディスパッチ・キューの2種類があります。この違いを理解するのは簡単です。シリアル・キューは、与えられたコマンドを予測可能な順序で実行します。シリアル・ディスパッチ・キューに2つのタスクをスケジュールした場合、最初のタスクが完了した後に2番目のタスクが実行されます。メリットは予測可能性です。欠点はパフォーマンスの低下です。2つ目のタスクは、1つ目のタスクの実行が終わるまで待つ必要があります。

コンカレントキューは、性能のために予測可能性を犠牲にするという点で異なります。コンカレントキューにスケジューリングされたタスクは、並列または同時に実行されます。コンカレントディスパッチキューに2つのタスクをスケジューリングした場合、2つ目のタスクは、1つ目のタスクの実行が終了する前にスケジューリングすることができます。これは、2つ目のタスクが1つ目のタスクよりも先に実行を終了する可能性があることを意味します。

簡単な例で説明しましょう。3つの画像をダウンロードし、それぞれの画像を画像ビューに表示する簡単なアプリケーションを作りました。このアプリケーションには2つのボタンが表示されています。左側のボタンは Serial と書かれており、シリアル・ディスパッチ・キューで処理を行います。右側のボタンは Concurrent と表示され、コンカレントディスパッチキューで処理を行います。

この違いを説明する前に、ViewController クラスの実装を見てみましょう。このクラスは、シリアルディスパッチキューとコンカレントディスパッチキューへの参照を保持しています。左のボタンがタップされると、download(using:) メソッドが呼び出され、引数としてシリアルディスパッチキューへの参照が渡されます。右のボタンがタップされた場合、 download(using:) メソッドが呼び出され、引数としてコンカレントディスパッチ・キューへの参照が渡されます。この例では、明示的にシリアル・キューとコンカレントディスパッチ・キューを作成しています。これはこれでいいのですが、 Grand Central Dispatch にディスパッチキューへの参照を求める方が一般的です。シリアルキューとコンカレントディスパッチキューを作成しても、それらは Grand Central Dispatch によって管理されます。

最も興味深いのは、 download(using:) メソッド の実装です。 image viewをリセットし、各 image view のアクティビティ・インジケータ・ビューを開始し、 download(using:) メソッド に渡されるディスパッチ・キュー上の作業ブロックをスケジュールします。 Grand Central Dispatch の API に慣れていなくても問題ありません。考え方はいたってシンプルです。

アプリケーションは URL インスタンスを使って Data インスタンス を作成します。画像データを使って UIImage インスタンス を作成し、画像ビューの image プロパティ に代入します。画像データのダウンロードと UIImage インスタンス の作成はバックグラウンドで行われます。

重要な違いは、シリアル・ディスパッチ・キューは与えられた作業を連続的または逐次的に実行することです。つまり、2つ目の画像の画像データは、1つ目の画像の画像データのダウンロードが完了した後に取得されます。3枚目の画像の画像データは、2枚目の画像の画像データのダウンロードが完了した後にフェッチされます。そうであれば、まず1枚目の画像が表示され、次に2枚目の画像が表示され、最後に3枚目の画像が表示されるはずです。つまり、画像は順次ダウンロードされ、表示されます。

これは、コンカレントディスパッチ・キューに作業をスケジューリングする場合には当てはまりません。このシナリオでは、コンカレントキューにディスパッチしたタスクは並列に実行されます。1枚目と3枚目の画像データのダウンロードが終わる前に、アプリケーションが2枚目の画像データのダウンロードを終えている可能性があります。このようなシナリオでは、1つ目のタスクが2つ目のタスクより前にスケジュールされていたとしても、2つ目の画像は1つ目の画像より先に表示されます。

理屈はここまでです。アプリケーションを実行し、何が起こるか見てみましょう。左側のボタンをタップすると、画像は予想通りの順番で、上から下に表示されます。右側のボタンをタップすると、画像は予測できない順番で表示されます。この違いを説明するために、3つ目の画像のファイルサイズを他の2つの画像よりもわざと小さくしています。3番目の画像の画像データをフェッチするタスクが最後にスケジュールされているにもかかわらず、3番目の画像が最初に表示されます。

メイン・キューとグローバル・キュー

例ではシリアルキューとコンカレントディスパッチキューを手動で作成しました。しかし、 Grand Central Dispatch に既存のディスパッチキューを問い合わせる方が一般的です。全てのアプリケーションは、メインキューといくつかのバックグラウンドキューにアクセスすることができます。

その名の通り、メインキューはメインスレッドに関連しています。これは何を意味するのでしょうか?メイン・キューにスケジューリングされた仕事は、メイン・スレッドで実行されることが保証されます。これは、ユーザーインターフェイスの更新など、あるタスクがメインスレッドでの実行が保証されていることを確認する必要がある場合に便利です。メイン・キューとメイン・スレッドについては、次のエピソードで詳しく説明します。

この例では、アプリケーションは Grand Central Dispatch を利用して、メインスレッド上で image view のimage プロパティを更新しています。アプリケーションは Grand Central Dispatch にメインキューへの参照を要求し、 image view のイメージプロパティが設定されるクロージャ(作業ブロック)を渡します。

また、 Grand Central Dispatch は、バックグラウンド・スレッドのプール上で作業を実行する、いくつかのコンカレントキューも管理します。これらのディスパッチキューはグローバルキューとしても知られています。コンカレントキューは実行順序を気にしないことを覚えておいてください。予測可能性を犠牲にしても、パフォーマンスが重要なのです。

次は?

このエピソードで、スレッド、キュー、並行処理など、もう少し高度な概念についてご理解いただけたと思います。これらの概念が何を意味するのかを理解するのに時間をかければ、 Grand Central Dispatch のような技術での作業がより簡単になります。次のエピソードでは、メインスレッドにズームインします。メインスレッドとは何か?

【翻訳元の記事】

Swift and Cocoa Essentials
Threads, Queues, and Concurrency
https://cocoacasts.com/swift-and-cocoa-fundamentals-threads-queues-and-concurrency

Discussion