Swift 6.0 beta 日本語化計画 : Swift 6.0 beta
同時実行
Swift には、構造化された方法で非同期および並列コードを記述するためのサポートが組み込まれています。非同期コード は中断して後で再開できますが、一度に実行できるのはプログラムの 1 つの部分だけです。あなたのプログラム内のコードを中断および再開すると、UI の更新などの短期的な操作を続行しながら、ネットワーク経由でのデータの fetch やファイルの解析などの長時間実行される操作を続行できます。並列コード は、複数のコードの部分が同時に実行されることを意味します。たとえば、4 コアのプロセッサを搭載したコンピューターは、4 つのコードの部分を同時に実行でき、各コアがタスクの 1 つを実行します。並列および非同期コードを使用するプログラムは、一度に複数の操作を実行し、外部システムを待機している操作を中断します。
並列または非同期コードによるスケジューリングの柔軟性の向上にはまた、複雑さの増加という代償も伴います。Swift を使用すると、コンパイル時のチェックを可能にする方法で意図を表現できます。たとえば、アクターを使用して可変状態に安全にアクセスできます。ただし、低速またはバグのあるコードに同時実行性を追加しても、それが高速または正確になるという保証はありません。実際、同時実行性を追加すると、コードのデバッグがもっと難しくなる可能性さえあります。ただし、同時実行が必要なコードでの同時実行に対する Swift の言語レベルのサポートを使用すると、Swift はコンパイル時に問題を検出するのに役立ちます。
この章の残りの部分では、同時並行 という用語を使用して、非同期コードと並列コードのこの一般的な組み合わせを指します。
Swift の言語サポートを使用せずに同時実行コードを作成することは可能ですが、そのコードは読みにくくなる傾向があります。たとえば、以下のコードは写真の名前のリストをダウンロードし、そのリストの最初の写真をダウンロードして、その写真をユーザーに表示します。
この単純なケースでも、一連の完了ハンドラーとしてコードを作成する必要があるため、入れ子にされたクロージャを作成することになります。このスタイルでは、深い入れ子になった、より複雑なコードはすぐに扱いにくくなります。
非同期関数 または 非同期メソッド は、実行の途中で中断できる特別な種類の関数またはメソッドです。これは、完了するまで実行するか、エラーを throw するか、決して戻らない、通常の同期関数およびメソッドとは対照的です。非同期関数または非同期メソッドは、これら 3 つのいずれかを実行しますが、何かを待っているときに途中で中断することもできます。非同期関数または非同期メソッドの本体内で、実行を中断できるこれらの場所をそれぞれマークして下さい。
関数またはメソッドが非同期であることを示すには、throws を使用して throw する関数をマークする方法と同様に、パラメータの後の宣言で async キーワードを記述します。関数またはメソッドが値を返す場合は、戻り矢印 (->) の前に async を記述します。たとえば、ギャラリー内の写真の名前を取得する方法は以下のとおりです。
非同期で throw する関数またはメソッドの場合、throws の前に async を記述します。
非同期メソッドを呼び出すと、そのメソッドが戻るまで実行が中断されます。呼び出しの前に await を記述して、可能な中断ポイントをマークします。これは、throw する関数を呼び出すときに try を記述して、エラーが発生した場合にプログラムの流れが変更される可能性があることをマークする事に似ています。非同期メソッド内では、別の非同期メソッドを呼び出した場合に のみ 実行の流れが中断されます — 中断が暗黙的または先制することは決してありません — つまり、考えられるすべての中断ポイントが await でマークされます。あなたのコード内で考えられる中断ポイントをすべてマークすると、同時実行コードが読みやすく、理解しやすくなります。
たとえば、以下のコードは、ギャラリー内のすべての写真の名前を取得してから、最初の写真を表示します。
listPhotos(inGallery:) と downloadPhoto(named:) 関数は両方ともネットワークリクエストを行う必要があるため、完了するまでに比較的長い時間がかかる場合があります。戻り矢印の前に async を記述して両方を非同期にすることで、このコードが写真の準備が整うまで待機している間、アプリの残りのコードを実行し続けることができます。
上記の例の同時実行の性質を理解するために、可能な実行順序の 1 つを以下に示します。
await でマークされたコード内の可能な中断ポイントは、非同期関数またはメソッドが戻るのを待っている間に、現在のコードの一部が実行を中断する可能性があることを示しています。これは、舞台裏で Swift が現在のスレッドでのコードの実行を中断し、代わりにそのスレッド上で他のコードを実行するため、スレッドの譲歩 とも呼ばれます。await を含むコードは実行を中断できる必要があるため、プログラム内の特定の場所でのみ非同期関数またはメソッドを呼び出すことができます。
Task.yield() メソッドを呼び出すことで、一時停止ポイントを明示的に挿入できます。
ビデオをレンダリングするコードが同期していると仮定すると、一時停止ポイントは全く含まれません。ビデオをレンダリングする作業にも長い時間がかかります。ただし、Task.yield() を定期的に呼び出して、一時停止ポイントを明示的に追加することができます。この方法で長時間実行されるコードを構造化すると、Swift はこのタスクの進捗と、プログラム内の他のタスクの作業の進捗との間でバランスをとることができます。
Task.sleep(for:tolerance: Clock:) メソッドは、同時実行の仕組みを学ぶために簡単なコードを作成する場合に役立ちます。このメソッドは、少なくとも与えられた時間、現在のタスクを一時停止します。以下は、sleep(for:tolerance: Clock:) を使用してネットワーク操作の待機をシミュレートする listPhotos(inGallery:) 関数のバージョンです。
上記のコードの listPhotos(inGallery:) のバージョンは、Task.sleep(until:tolerance: Clock:) への呼び出しでエラーが throw される可能性があるため、非同期かつスローする両方です。このバージョンの listPhotos(inGallery:) を呼び出すときは、try と await の両方を記述します。
非同期関数は throw する関数といくつかの類似点があります。非同期関数または throw する関数を定義するときは、それを async または throws でマークし、その関数への呼び出しを await または try でマークします。非同期関数は、throw する関数が別の throw する関数を呼び出せるのと同じように、別の非同期関数を呼び出せます。
ただし、非常に重要な違いがあります。do-catch ブロックで throw するコードを包み込んてエラーを処理したり、Result を使用してコードのエラーを別の場所に保存して処理したりできます。これらのアプローチにより、throw しないコードから throw する関数を呼び出せます。例えば:
対照的に、同期コードから呼び出して結果を待つことができるように非同期コードを包み込める安全な方法はありません。Swift 標準ライブラリは、この安全でない機能を意図的に省略しています。これを自分で実装しようとすると、微妙な競合、スレッドの問題、およびデッドロックなどの問題が発生する可能性があります。既存のプロジェクトに同時実行コードを追加する場合は、上から下に作業します。具体的には、同時実行を使用するようにコードの最上位層を変換することから始め、次に、プロジェクトのアーキテクチャを一度に 1 層ずつ処理しながら、その層が呼び出す関数とメソッドの変換を開始します。同期コードは非同期コードを呼び出すことが全くできないため、ボトムアップアプローチを取る方法はありません。
前のセクションの listPhotos(inGallery:) 関数は、配列のすべての要素の準備が整った後、一度に配列全体を非同期的に返します。もう 1 つの方法は、非同期シーケンス を使用して、一度に 1 つのコレクションの要素を待機することです。非同期シーケンスの反復処理は以下のようになります。
上記の例では、通常の for-in ループを使用する代わりに、for とその後に await を記述しています。非同期の関数またはメソッドを呼び出す場合と同様に、await を記述すると中断ポイントの可能性が示されます。for-await-in ループは、次の要素が利用可能になるのを待っているときに、各反復の開始時に実行を中断する可能性があります。
Sequence プロトコルに準拠を追加することによって for-in ループで独自の型を使用できるのと同じ方法で、AsyncSequence (AsyncSequence) プロトコルに準拠を追加することによって for-await-in ループで独自の型を使用できます。
await を使用して非同期関数を呼び出すと、一度に 1 つのコードしか実行されません。非同期コードの実行中、呼び出し元はそのコードが終了するのを待ってから、次のコード行の実行に移ります。たとえば、ギャラリーから最初の 3 枚の写真を取得するには、以下のように downloadPhoto(named:) 関数への 3 回の呼び出しを待機 (await) できます。
このアプローチには重大な欠点があります。ダウンロードは非同期であり、進行中に他の作業を実行できますが、downloadPhoto(named:) への呼び出しは一度に 1 つしか実行されません。次の写真がダウンロードを開始する前に、各写真が完全にダウンロードされます。ただし、これらの操作を待つ必要はありません。各写真を独立してダウンロードすることも、同時にダウンロードすることもできます。
非同期関数を呼び出して、その周りのコードと並行して実行するには、定数を定義するときに let の前に async を記述し、定数を使用するたびに await を記述して下さい。
この例では、 downloadPhoto(named:) への 3 つの呼び出しはすべて、前の呼び出しが完了するのを待たずに開始されます。利用可能なシステムリソースが十分にある場合は、同時に実行できます。コードは関数の結果を待機するのに中断しないため、これらの関数呼び出しはいずれも await でマークされていません。代わりに、photos が定義されている行まで実行が続きます。その時点で、プログラムはこれらの非同期呼び出しの結果を必要とするため、3 つの写真すべてのダウンロードが完了するまで実行を一時停止する await を記述して下さい。
これら 2 つのアプローチの違いについて、以下のように考えることができます。
これらの両方のアプローチを同じコードに混在させることもできます。
タスク は、プログラムの一部として非同期に実行できる作業単位です。すべての非同期コードは、何らかのタスクの一部として実行されます。タスク自体は一度に 1 つのことしか実行しませんが、複数のタスクを作成すると、Swift はそれらを同時に実行するようにスケジュール化できます。
前のセクションで説明した async-let 構文により、子タスクが暗黙に作成されます。この構文は、プログラムで実行する必要があるタスクがすでにわかっている場合にうまく機能します。また、タスクグループ(TaskGroup (TaskGroup) のインスタンス)を作成し、そのグループに子タスクを明示的に追加することもできます。これにより、優先度とキャンセルをより詳細に制御でき、動的な数のタスクを作成できます。
タスクは階層に配置されます。与えられたタスクグループ内の各タスクには同じ親タスクがあり、各タスクは子タスクを持つことができます。タスクとタスクグループの間には明示的な関係があるため、このアプローチは 構造化された同時実行性 と呼ばれます。タスク間の明示的な親子関係には、いくつかの利点があります。
以下は、任意の数の写真を処理する、写真をダウンロードするコードの別のバージョンです。
上記のコードは、新しいタスク グループを作成し、ギャラリー内の各写真をダウンロードするための子タスクを作成します。Swift は、条件が許す限りこれらのタスクをできるだけ多く同時に実行します。子タスクが写真のダウンロードを完了するとすぐに、その写真が表示されます。子タスクが完了する順序については保証がないため、このギャラリーの写真は任意の順序で表示されます。
上記のコードリストでは、各写真がダウンロードされてから表示されるため、タスクグループは結果を全く返しません。結果を返すタスクグループの場合は、withTaskGroup(of:returning:body:) に渡すクロージャ内に結果を蓄積するコードを追加して下さい。
前の例と同様に、この例では写真ごとに子タスクを作成してダウンロードします。 前の例とは異なり、for-await-in ループは次の子タスクが終了するのを待ち、そのタスクの結果を結果の配列に追加して、すべての子タスクが終了するまで待機し続けます。最後に、タスクグループは、ダウンロードされた写真の配列を全体的な結果として返します。
Swift の同時実行では、協調キャンセルモデルが使用されます。各タスクは、実行中の適切な時点でキャンセルされたかどうかを確認し、キャンセルに適切に応答します。タスクが実行している作業に応じて、キャンセルへの応答は通常、次のいずれかを意味します。
写真が大きい場合やネットワークが遅い場合は、写真のダウンロードに時間がかかります。すべてのタスクが完了するのを待たずにユーザーがこの作業を停止できるようにするには、タスクがキャンセルされているかどうかを確認し、キャンセルされている場合は実行を停止する必要があります。タスクでこれを行うには 2 つの方法があります。Task.checkCancellation() 型メソッドを呼び出すことと、Task.isCancelled 型プロパティを読み取ることです。タスクがキャンセルされた場合、checkCancellation() を呼び出すとエラーが throws されます。throw するタスクは、タスクの外にエラーを伝播し、タスクのすべての作業を停止する可能性があります。これには、実装と理解が簡単であるという利点があります。柔軟性を高めるには、isCancelled プロパティを使用します。これにより、ネットワーク接続の終了や一時ファイルの削除など、タスクの停止の一部としてクリーンアップ作業を実行できます。
上記のコードには、以前のバージョンからいくつかの変更が加えられています。
キャンセルの即時通知が必要な作業の場合は、Task.withTaskCancellationHandler(operation:onCancel:) メソッドを使用します。例えば:
キャンセルハンドラーを使用する場合でも、タスクのキャンセルはまだ協調的です。タスクは完了するまで実行されるか、キャンセルを確認して早期に停止します。キャンセルハンドラーの開始時にタスクはまだ実行中であるため、タスクとそのキャンセルハンドラーの間で状態を共有しないようにしてください。競合状態が発生する可能性があります。
前のセクションで説明した同時実行に対する構造化されたアプローチに加えて、Swift はまた構造化されていない同時実行もサポートしています。タスクグループの一部であるタスクとは異なり、構造化されていないタスク には親タスクがありません。プログラムが必要とするどんな方法でも構造化されていないタスクを管理するには完全な柔軟性がありますが、それらの正確性についても完全に責任があります。現在のアクター上で実行される構造化されていないタスクを作成するには、Task.init(priority:operation:) イニシャライザを呼び出します。現在のアクターの一部ではない構造化されていないタスク (具体的には 分離タスク と呼ばれる) を作成するには、Task.detached(priority:operation:) クラスメソッドを呼び出します。これらの操作はどちらも、操作できるタスクを返します。たとえば、結果を待つか、キャンセルする事です。
分離タスクの管理の詳細については、タスク(Task) を参照してください。
タスクを使用して、プログラムを分離された同時実行部分に分割できます。タスクは互いに分離されているため、同時に実行しても安全ですが、タスク間で情報を共有する必要があります。アクターを使用すると、同時実行コード間で情報を安全に共有できます。
クラスと同様に、アクターは参照型であるため、クラスは参照型 での値型と参照型の比較は、クラスだけでなくアクターにも適用されます。クラスとは異なり、アクターは、一度に 1 つのタスクのみが可変状態にアクセスできるため、複数のタスクでのコードがアクターの同じインスタンスを操作しても安全です。たとえば、気温を記録するアクターは以下のとおりです。
actor キーワードを使用してアクターを導入し、その後にその定義を中かっこのペアで囲みます。TemperatureLogger アクターには、アクターの外の他のコードがアクセスできるプロパティがあり、アクター内のコードのみが最大値を更新できるように max プロパティを制限します。
構造体やクラスと同じ初期化構文を使用して、アクターのインスタンスを作成して下さい。アクターのプロパティまたはメソッドにアクセスするときは、await を使用して潜在的な中断ポイントをマークして下さい。例えば:
この例では、logger.max へのアクセスが中断ポイントになる可能性があります。アクターは一度に 1 つのタスクのみが可変状態にアクセスできるため、別のタスクのコードが既にロガーを操作している場合、このコードはプロパティへのアクセスを待機している間中断されます。
対照的に、アクターの一部であるコードは、アクターのプロパティにアクセスするときに await を書き込みません。たとえば、TemperatureLogger を新しい温度で更新するメソッドは以下のとおりです。
update(with:) メソッドはすでにアクター上で実行されているため、max などのプロパティへのアクセスを await でマークしません。このメソッドはまた、アクターが一度に 1 つのタスクしか可変状態を操作させない理由の 1 つも示しています。アクターの状態の何らかの更新によって、一時的に不変条件が壊れます。TemperatureLogger のアクターは、温度と最高温度のリストを追跡し、新しい測定値を記録すると最高温度を更新します。更新の途中で、新しい測定値を追加した後、max を更新する前に、温度ロガーが一時的に矛盾した状態になります。複数のタスクが同じインスタンスを同時に操作しないようにすることで、以下の一連のイベントのような問題を防ぐことができます。
この場合、アクターへのアクセスが update(with:) への呼び出しの途中でインターリーブされ、データが一時的に無効になるため、別の場所で実行されているコードが誤った情報を読み取ることになります。Swift のアクターを使用すると、それらの状態には一度に 1 つの操作しか許可されず、そのコードは await が中断ポイントをマークする場所でのみ中断できるため、この問題を回避できます。update(with:) には中断ポイントが含まれていないため、他のコードは更新中にはデータにアクセスできません。
アクターの外部のコードが構造体やクラスのプロパティにアクセスするなど、これらのプロパティに直接アクセスしようとすると、コンパイル時エラーが発生します。例えば:
アクターのプロパティはそのアクターの分離されたローカル状態の一部であるため、await を書き込まずに logger.max にアクセスすると失敗します。このプロパティにアクセスするコードはアクターの一部として実行する必要があります。これは非同期操作であり、await を記述する必要があります。Swift は、アクター上で実行されているコードのみがそのアクターのローカル状態にアクセスできることを保証します。この保証は アクター分離 として知られています。
Swift の同時実行モデルの以下の側面が連携して、共有の可変状態についての推論を容易にします。
これらの保証により、await を含まず、アクター内にあるコードは、プログラム内の他の場所で一時的に無効な状態が観察されるリスクを負うことなく、更新を行うことができます。たとえば、以下のコードは測定温度を華氏から摂氏に変換します。
上記のコードは、測定値の配列を一度に 1 つずつ変換します。マップ操作の進行中、一部の温度は華氏で表示され、その他は摂氏で表示されます。ただし、どのコードにも await が含まれていないため、このメソッドには潜在的な中断ポイントがありません。このメソッドが変更する状態はアクターに属し、そのコードがアクター上で実行される場合を除き、コードの読み取りや変更からアクターを保護します。これは、単位変換の進行中に、他のコードが部分的に変換された温度のリストを読み取る方法がないことを意味します。
潜在的な中断ポイントを省略して一時的に無効な状態を保護するコードをアクターで書き込むだけでなく、そのコードを同期メソッドに移動することもできます。上記の convertFahrenheitToCelsius( ) メソッドは同期メソッドであるため、潜在的な中断ポイントが 決して 含まれないことが保証されています。この関数は、データモデルの一貫性を一時的に失わせるコードをカプセル化し、作業を完了してデータの一貫性が回復するまでは他のコードを実行できないことを、コードを読む人が容易に認識できるようにします。将来、この関数に同時実行コードを追加して中断ポイントが発生する可能性がある場合、バグが発生する代わりにコンパイル時エラーが発生するでしょう。
タスクとアクターを使用するとプログラムを、安全に同時実行できる部分に分割できます。タスクまたはアクターのインスタンス内で、可変状態 (変数やプロパティなど) を含むプログラムの部分は、同時実行ドメイン と呼ばれます。データには変更可能な状態が含まれているため、一部の種類のデータは同時実行ドメイン間で共有できませんが、アクセスの重複を防ぐことはできません。
ある同時実行ドメインから別のドメインに共有できる型は、Sendable な型 と呼ばれます。たとえば、アクターメソッドを呼び出すときに引数として渡すか、タスクの結果として返すことができます。この章の前半の例では、同時実行ドメイン間で渡されるデータを常に安全に共有できる単純な値型を使用しているため、送信可能性 (sendability) については説明しませんでした。対照的に、一部の型では同時実行ドメイン間で安全に渡すことができません。たとえば、変更可能なプロパティを含み、それらのプロパティへのアクセスをシリアル化しないクラスは、異なるタスク間でそのクラスのインスタンスを渡すときに、予測できない誤った結果を生成する可能性があります。
Sendable プロトコルへの準拠を宣言することにより、型を sendable (送信可能) としてマークします。そのプロトコルにはコード要件はありませんが、Swift が強制する意味要件があります。一般に、型を送信可能 (sendable) にする方法は 3 つあります。
意味的要件の詳細なリストについては、Sendable プロトコルリファレンスを参照してください。
送信可能 (sendable) なプロパティのみを持つ構造体や、送信可能な関連する値のみを持つ列挙型など、一部の型は常に送信可能です。例えば:
TemperatureReading は送信可能 (sendable) なプロパティのみを持つ構造体であり、この構造体は public または @usableFromInline としてマークされていないため、暗黙的に送信可能 (sendable) です。Sendable プロトコルへの準拠が暗示される構造体のバージョンを以下に示します。
型を送信不可として明示的にマークするには、Sendable プロトコルへの暗黙の準拠をオーバーライドし、以下の拡張機能を使用します。
上記のコードは、POSIX ファイル記述子のラッパーの一部を示しています。ファイル記述子のインターフェイスでは、開いているファイルの識別と操作に整数が使用され、整数値は送信可能ですが、ファイル記述子を同時実行ドメイン間で送信するのは安全ではありません。
上記のコードでは、FileDescriptor は暗黙的に送信可能であるための基準を満たす構造体です。ただし、この拡張機能により Sendable への準拠が無効になり、その型を送信可能にすることができなくなります。