Swift 5.8 日本語化計画 : Swift 5.8
同時実行
Swift には、構造化された方法で非同期および並列コードを記述するためのサポートが組み込まれています。非同期コード は中断して後で再開できますが、一度に実行できるのはプログラムの 1 つの部分だけです。あなたのプログラム内のコードを中断および再開すると、UI の更新などの短期的な操作を続行しながら、ネットワーク経由でのデータの fetch やファイルの解析などの長時間実行される操作を続行できます。並列コード は、複数のコードが同時に実行されることを意味します。たとえば、4 コアのプロセッサを搭載したコンピューターは、4 つのコードを同時に実行でき、各コアがタスクの 1 つを実行します。並列および非同期コードを使用するプログラムは、一度に複数の操作を実行します。外部システムを待機している操作を中断し、このコードをメモリセーフな方法で簡単に記述できるようにします。
並列または非同期コードによるスケジューリングの柔軟性の向上にはまた、複雑さの増加という代償も伴います。Swift を使用すると、コンパイル時のチェックを可能にする方法で意図を表現できます。たとえば、アクターを使用して可変状態に安全にアクセスできます。ただし、低速またはバグのあるコードに同時実行性を追加しても、それが高速または正確になるという保証はありません。実際、同時実行性を追加すると、コードのデバッグがもっと難しくなる可能性さえあります。 ただし、同時実行が必要なコードの同時実行に対する Swift の言語レベルのサポートを使用すると、Swift はコンパイル時に問題を検出するのに役立ちます。
この章の残りの部分では、同時並行 という用語を使用して、非同期コードと並列コードのこの一般的な組み合わせを指します。
Swift の言語サポートを使用せずに同時実行コードを作成することは可能ですが、そのコードは読みにくくなる傾向があります。たとえば、以下のコードは写真の名前のリストをダウンロードし、そのリストの最初の写真をダウンロードして、その写真をユーザーに表示します。
- listPhotos(inGallery: "Summer Vacation") { photoNames in
- let sortedNames = photoNames.sorted()
- let name = sortedNames[0]
- downloadPhoto(named: name) { photo in
- show(photo)
- }
- }
この単純なケースでも、一連の完了ハンドラーとしてコードを作成する必要があるため、入れ子にされたクロージャーを作成することになります。このスタイルでは、深い入れ子になった、より複雑なコードはすぐに扱いにくくなります。
非同期関数の定義と呼び出し
非同期関数 または 非同期メソッド は、実行の途中で中断できる特別な種類の関数またはメソッドです。これは、完了するまで実行するか、エラーを throw するか、決して戻らない、通常の同期関数およびメソッドとは対照的です。非同期関数または非同期メソッドは、これら 3 つのいずれかを実行しますが、何かを待っているときに途中で中断することもできます。非同期関数または非同期メソッドの本体内で、実行を中断できるこれらの場所をそれぞれマークします。
関数またはメソッドが非同期であることを示すには、throws を使用して throw する関数をマークする方法と同様に、パラメーターの後の宣言で async キーワードを記述します。関数またはメソッドが値を返す場合は、戻り矢印 (->) の前に async を記述します。たとえば、ギャラリー内の写真の名前を取得する方法は以下のとおりです。
- func listPhotos(inGallery name: String) async -> [String] {
- let result = // ... some asynchronous networking code ...
- return result
- }
非同期で throw する関数またはメソッドの場合、throws の前に async を記述します。
非同期メソッドを呼び出すと、そのメソッドが戻るまで実行が中断されます。呼び出しの前に await を記述して、可能な中断ポイントをマークします。これは、throw する関数を呼び出すときに try を記述して、エラーが発生した場合にプログラムの流れが変更される可能性があることをマークするようなものです。非同期メソッド内では、別の非同期メソッドを呼び出した場合に のみ 実行の流れが中断されます — 中断が暗黙的または先制することはありません — つまり、考えられるすべての中断ポイントが await でマークされます。
たとえば、以下のコードは、ギャラリー内のすべての写真の名前を取得してから、最初の写真を表示します。
- let photoNames = await listPhotos(inGallery: "Summer Vacation")
- let sortedNames = photoNames.sorted()
- let name = sortedNames[0]
- let photo = await downloadPhoto(named: name)
- show(photo)
listPhotos(inGallery:) と downloadPhoto(named:) 関数は両方ともネットワークリクエストを行う必要があるため、完了するまでに比較的長い時間がかかる場合があります。戻り矢印の前に async を記述して両方を非同期にすることで、このコードが写真の準備が整うまで待機している間、アプリの残りのコードを実行し続けることができます。
上記の例の同時実行の性質を理解するために、可能な実行順序の 1 つを以下に示します。
- コードは最初の行から実行を開始し、最初の await まで実行します。listPhotos(inGallery:) 関数を呼び出し、その関数が戻るのを待つ間、実行を中断します。
- このコードの実行が中断されている間、同じプログラム内の他の同時実行コードが実行されます。たとえば、長時間実行されるバックグラウンドのタスクが、新しい写真ギャラリーのリストを更新し続ける場合があります。そのコードはまた、await でマークされた次の中断ポイントまで、または完了するまで実行されます。
- listPhotos(inGallery:) が戻った後、このコードはその点から実行を継続します。返された値を photoNames に割り当てます。
- sortedNames と name を定義する行は、通常の同期コードです。これらの行には何も await とマークされていないため、可能な中断ポイントは全くありません。
- 次の await は、downloadPhoto(named:) 関数の呼び出しをマークします。このコードは、その関数が戻るまで実行を再び中断し、他の同時実行コードに実行する機会を与えます。
- downloadPhoto(named:) が返された後、その戻り値が photo に割り当てられ、show(_:) を呼び出すときに引数として渡されます。
await でマークされたコード内の可能な中断ポイントは、非同期関数またはメソッドが戻るのを待っている間に、現在のコードの一部が実行を中断する可能性があることを示しています。これは、舞台裏で Swift が現在のスレッドでのコードの実行を中断し、代わりにそのスレッド上で他のコードを実行するため、スレッドの譲歩 とも呼ばれます。await を含むコードは実行を中断できる必要があるため、プログラム内の特定の場所でのみ非同期関数またはメソッドを呼び出すことができます。
可能な中断ポイント間のコードは、他の同時実行コードからの中断の可能性なしで、順次実行されます。たとえば、以下のコードは、あるギャラリーから別のギャラリーに写真を移動します。
- let firstPhoto = await listPhotos(inGallery: "Summer Vacation")[0]
- add(firstPhoto, toGallery: "Road Trip")
- // At this point, firstPhoto is temporarily in both galleries.
- remove(firstPhoto, fromGallery: "Summer Vacation")
add(_:toGallery:) と remove(_:fromGallery:) の呼び出しの間に他のコードを実行する方法はありません。その間、最初の写真が両方のギャラリーに表示され、アプリの不変条件の 1 つが一時的に壊れます。このコードのチャンクに今後 await を追加してはならないことをさらに明確にするために、そのコードを同期関数にリファクタリングできます。
- func move(_ photoName: String, from source: String, to destination: String) {
- add(photoName, toGallery: destination)
- remove(photoName, fromGallery: source)
- }
- // ...
- let firstPhoto = await listPhotos(inGallery: "Summer Vacation")[0]
- move(firstPhoto, from: "Summer Vacation", to: "Road Trip")
上記の例では、move(_:from:to:) 関数が同期的であるため、中断の可能性がある点が決して含まれないことが保証されます。将来、この関数に同時実行コードを追加しようとすると、中断ポイントが発生する可能性があり、バグが発生する代わりにコンパイル時エラーが発生します。
- func listPhotos(inGallery name: String) async throws -> [String] {
- try await Task.sleep(until: .now + .seconds(2), clock: .continuous)
- return ["IMG001", "IMG99", "IMG0404"]
- }
非同期シーケンス
前のセクションの listPhotos(inGallery:) 関数は、配列のすべての要素の準備が整った後、一度に配列全体を非同期的に返します。もう 1 つの方法は、非同期シーケンス を使用して、一度に 1 つのコレクションの要素を待機することです。非同期シーケンスの反復処理は以下のようになります。
- import Foundation
- let handle = FileHandle.standardInput
- for try await line in handle.bytes.lines {
- print(line)
- }
上記の例では、通常の for-in ループを使用する代わりに、 for とその後に await を記述しています。非同期の関数またはメソッドを呼び出す場合と同様に、await を記述すると中断ポイントの可能性が示されます。for-await-in ループは、次の要素が利用可能になるのを待っているときに、各反復の開始時に実行を中断する可能性があります。
Sequence プロトコルに準拠を追加することによって for-in ループで独自の型を使用できるのと同じ方法で、AsyncSequence プロトコルに準拠を追加することによって for-await-in ループで独自の型を使用できます。
並列での非同期関数の呼び出し
await を使用して非同期関数を呼び出すと、一度に 1 つのコードしか実行されません。非同期コードの実行中、呼び出し元はそのコードが終了するのを待ってから、次のコード行の実行に移ります。たとえば、ギャラリーから最初の 3 枚の写真を取得するには、以下のように downloadPhoto(named:) 関数への 3 回の呼び出しを待機 (await) できます。
- let firstPhoto = await downloadPhoto(named: photoNames[0])
- let secondPhoto = await downloadPhoto(named: photoNames[1])
- let thirdPhoto = await downloadPhoto(named: photoNames[2])
- let photos = [firstPhoto, secondPhoto, thirdPhoto]
- show(photos)
このアプローチには重大な欠点があります。ダウンロードは非同期であり、進行中に他の作業を実行できますが、downloadPhoto(named:) への呼び出しは一度に 1 つしか実行されません。次の写真がダウンロードを開始する前に、各写真が完全にダウンロードされます。ただし、これらの操作を待つ必要はありません。各写真を独立してダウンロードすることも、同時にダウンロードすることもできます。
非同期関数を呼び出して、その周りのコードと並行して実行するには、定数を定義するときに let の前に async を記述し、定数を使用するたびに await を記述して下さい。
- async let firstPhoto = downloadPhoto(named: photoNames[0])
- async let secondPhoto = downloadPhoto(named: photoNames[1])
- async let thirdPhoto = downloadPhoto(named: photoNames[2])
- let photos = await [firstPhoto, secondPhoto, thirdPhoto]
- show(photos)
この例では、 downloadPhoto(named:) への 3 つの呼び出しはすべて、前の呼び出しが完了するのを待たずに開始されます。利用可能なシステムリソースが十分にある場合は、同時に実行できます。コードは関数の結果を待機するために中断しないため、これらの関数呼び出しはいずれも await でマークされていません。代わりに、photos が定義されている行まで実行が続きます。その時点で、プログラムはこれらの非同期呼び出しの結果を必要とするため、3 つの写真すべてのダウンロードが完了するまで実行を一時停止する await を記述して下さい。
これら 2 つのアプローチの違いについて、以下のように考えることができます。
これらの両方のアプローチを同じコードに混在させることもできます。
タスクとタスクグループ
タスクは、プログラムの一部として非同期に実行できる作業単位です。すべての非同期コードは、何らかのタスクの一部として実行されます。前のセクションで説明した async-let 構文により、子タスクが作成されます。タスクグループを作成し、そのグループに子タスクを追加することもできます。これにより、優先度とキャンセルをより詳細に制御でき、動的な数のタスクを作成できます。
タスクは階層に配置されます。タスクグループ内の各タスクには同じ親タスクがあり、各タスクは子タスクを持つことができます。タスクとタスクグループの間には明示的な関係があるため、このアプローチは 構造化された同時実行性 と呼ばれます。正確さの責任はあなたがいくらか負いますが、タスク間の明示的な親子関係により、Swift はキャンセルの伝播などのいくつかの動作を処理し、Swift はコンパイル時にいくつかのエラーを検出できます。
- await withTaskGroup(of: Data.self) { taskGroup in
- let photoNames = await listPhotos(inGallery: Summer Vacation")
- for name in photoNames {
- taskGroup.addTask { await downloadPhoto(named: name) }
- }
- }
タスクグループの詳細については、TaskGroup を参照してください。
構造化されていない同時実行
前のセクションで説明した同時実行に対する構造化されたアプローチに加えて、Swift はまた構造化されていない同時実行もサポートしています。タスクグループの一部であるタスクとは異なり、構造化されていないタスク には親タスクがありません。プログラムが必要とするどんな方法でも構造化されていないタスクを管理するには完全な柔軟性がありますが、それらの正確性についても完全に責任があります。現在のアクター上で実行される構造化されていないタスクを作成するには、Task.init(priority:operation:) イニシャライザを呼び出します。現在のアクターの一部ではない構造化されていないタスク (具体的には 分離タスク と呼ばれる) を作成するには、Task.detached(priority:operation:) クラスメソッドを呼び出します。これらの操作はどちらも、操作できるタスクを返します。たとえば、結果を待つか、キャンセルする事です。
- let newPhoto = // ... some photo data ...
- let handle = Task {
- return await add(newPhoto, toGalleryNamed: "Spring Adventures")
- }
- let result = await handle.value
分離タスクの管理の詳細については、タスク を参照してください。
タスクのキャンセル
Swift の同時実行は、協調的なキャンセルモデルを使用します。各タスクは、実行中の適切な時点でキャンセルされたかどうかを確認し、適切な方法でキャンセルに応答します。行っている作業に応じて、通常は以下のいずれかを意味します。
キャンセルを確認するには、タスクがキャンセルされた場合に CancellationError を throw する Task.checkCancellation() を呼び出すか、Task.isCancelled の値を確認して独自のコードでキャンセルを処理します。たとえば、ギャラリーから写真をダウンロードするタスクでは、部分的なダウンロードを削除し、ネットワーク接続を閉じる必要があるかもしれません。
キャンセルを手動で伝播するには、Task.cancel() を呼び出します。
アクター
タスクを使用して、プログラムを分離された同時実行部分に分割できます。タスクは互いに分離されているため、同時に実行しても安全ですが、タスク間で情報を共有する必要があります。アクターを使用すると、同時実行コード間で情報を安全に共有できます。
クラスと同様に、アクターは参照型であるため、クラスは参照型 での値型と参照型の比較は、クラスだけでなくアクターにも適用されます。クラスとは異なり、アクターは、一度に 1 つのタスクのみが可変状態にアクセスできるため、複数のタスクのでコードがアクターの同じインスタンスを操作しても安全です。たとえば、気温を記録するアクターは以下のとおりです。
- actor TemperatureLogger {
- let label: String
- var measurements: [Int]
- private(set) var max: Int
- init(label: String, measurement: Int) {
- self.label = label
- self.measurements = [measurement]
- self.max = measurement
- }
- }
actor キーワードを使用してアクターを導入し、その後にその定義を中かっこで囲みます。TemperatureLogger アクターには、アクターの外の他のコードがアクセスできるプロパティがあり、アクター内のコードのみが最大値を更新できるように max プロパティを制限します。
構造体やクラスと同じ初期化構文を使用して、アクターのインスタンスを作成します。アクターのプロパティまたはメソッドにアクセスするときは、await を使用して潜在的な中断ポイントをマークして下さい。例えば:
- let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
- print(await logger.max)
- // Prints "25"
この例では、logger.max へのアクセスが中断ポイントになる可能性があります。アクターは一度に 1 つのタスクのみが可変状態にアクセスできるため、別のタスクのコードが既にロガーを操作している場合、このコードはプロパティへのアクセスを待機している間中断されます。
対照的に、アクターの一部であるコードは、アクターのプロパティにアクセスするときに await を書き込みません。たとえば、TemperatureLogger を新しい温度で更新するメソッドは以下のとおりです。
- extension TemperatureLogger {
- func update(with measurement: Int) {
- measurements.append(measurement)
- if measurement > max {
- max = measurement
- }
- }
- }
update(with:) メソッドはすでにアクター上で実行されているため、max などのプロパティへのアクセスを await でマークしません。このメソッドはまた、アクターが一度に 1 つのタスクしか可変状態を操作させない理由の 1 つも示しています。アクターの状態の何らかの更新によって、一時的に不変条件が壊れます。TemperatureLogger のアクターは、温度と最高温度のリストを追跡し、新しい測定値を記録すると最高温度を更新します。更新の途中で、新しい測定値を追加した後、max を更新する前に、温度ロガーが一時的に矛盾した状態になります。複数のタスクが同じインスタンスを同時に操作しないようにすることで、以下の一連のイベントのような問題を防ぐことができます。
- コードは update(with:) メソッドを呼び出します。最初に measurement 配列を更新します。
- コードが max を更新する前に、他の場所のコードが最大値と温度の配列を読み取ります。
- コードは max を変更して更新を終了します。
この場合、アクターへのアクセスが update(with:) への呼び出しの途中でインターリーブされ、データが一時的に無効になるため、別の場所で実行されているコードが誤った情報を読み取ることになります。Swift のアクターを使用すると、それらの状態には一度に 1 つの操作しか許可されず、コードは await が中断ポイントをマークする場所でのみ中断できるため、この問題を回避できます。update(with:) には中断ポイントが含まれていないため、他のコードは更新中にはデータにアクセスできません。
クラスのインスタンスの場合のように、アクターの外部からこれらのプロパティにアクセスしようとすると、コンパイル時エラーが発生します。例えば:
アクターのプロパティはそのアクターの分離されたローカル状態の一部であるため、await を書き込まずに logger.max にアクセスすると失敗します。Swift は、アクター内のコードのみがアクターのローカル状態にアクセスできることを保証します。この保証は、アクター分離 として知られています。
Sendable な型
タスクとアクターを使用するとプログラムを、安全に同時実行できる部分に分割できます。タスクまたはアクターのインスタンス内で、可変状態 (変数やプロパティなど) を含むプログラムの部分は、同時実行ドメイン と呼ばれます。データには変更可能な状態が含まれているため、一部の種類のデータは同時実行ドメイン間で共有できませんが、アクセスの重複を防ぐことはできません。
ある同時実行ドメインから別のドメインに共有できる型は、Sendable な型 と呼ばれます。たとえば、アクターメソッドを呼び出すときに引数として渡すか、タスクの結果として返すことができます。この章の前半の例では、同時実行ドメイン間で渡されるデータを常に安全に共有できる単純な値型を使用しているため、送信可能性 (sendability) については説明しませんでした。対照的に、一部の型では同時実行ドメイン間で安全に渡すことができません。たとえば、変更可能なプロパティを含み、それらのプロパティへのアクセスをシリアル化しないクラスは、異なるタスク間でそのクラスのインスタンスを渡すときに、予測できない誤った結果を生成する可能性があります。
Sendable プロトコルへの準拠を宣言することにより、型を sendable (送信可能) としてマークします。そのプロトコルにはコード要件はありませんが、Swift が強制する意味要件があります。一般に、型を送信可能 (sendable) にする方法は 3 つあります。
意味的要件の詳細なリストについては、Sendable プロトコルリファレンスを参照してください。
送信可能 (sendable) なプロパティのみを持つ構造体や、送信可能な関連する値のみを持つ列挙型など、一部の型は常に送信可能です。例えば:
- struct TemperatureReading: Sendable {
- var measurement: Int
- }
- extension TemperatureLogger {
- func addReading(from reading: TemperatureReading) {
- measurements.append(reading.measurement)
- }
- }
- let logger = TemperatureLogger(label: "Tea kettle", measurement: 85)
- let reading = TemperatureReading(measurement: 45)
- await logger.addReading(from: reading)
TemperatureReading は送信可能 (sendable) なプロパティのみを持つ構造体であり、この構造体は public または @usableFromInline としてマークされていないため、暗黙的に送信可能 (sendable) です。Sendable プロトコルへの準拠が暗示される構造体のバージョンを以下に示します。
- struct TemperatureReading {
- var measurement: Int
- }
前:エラー処理 次:型キャスト
トップへ
トップへ
トップへ
トップへ