Swift 6.2 日本語化計画 : Swift 6.2


同時実行


非同期操作を実行します。


Swift には、構造化された方法で非同期および並列コードを記述するためのサポートが組み込まれています。非同期コード は中断して後で再開できますが、一度に実行できるのはプログラムの 1 つの部分だけです。あなたのプログラム内のコードを中断および再開すると、UI の更新などの短期的な操作を続行しながら、ネットワーク経由でのデータの fetch やファイルの解析などの長時間実行される操作を続行できます。並列コード は、複数のコードの部分が同時に実行されることを意味します。たとえば、4 コアのプロセッサを搭載したコンピューターは、4 つのコードの部分を同時に実行でき、各コアがタスクの 1 つを実行します。並列および非同期コードを使用するプログラムは、一度に複数の操作を実行し、外部システムを待機している操作を中断します。この章の残りの部分では、非同期コードと並列コードの一般的な組み合わせを指すために 同時実行 という用語を使用します。


並列または非同期コードによるスケジューリングの柔軟性の向上にはまた、複雑さの増加という代償も伴います。並同時実行コードを書く場合、どのコードが同時に実行されるかは事前にわかりませんし、コードの実行順序も必ずしもわからない場合があります。同時実行コードでよくある問題は、複数のコードが共有の変更可能な状態にアクセスしようとしたときに発生します。これは データ競合 と呼ばれます。言語レベルの並行性サポートを使用すると、Swift はデータ競合を検出して防止し、ほとんどのデータ競合はコンパイル時エラーを生成します。一部のデータ競合はコードが実行されるまで検出されず、これらのデータ競合はコードの実行を終了させます。この章で説明するように、アクターと分離を使用してデータ競合から保護します。


注意: 以前に同時実行コードを作成したことがある場合は、スレッドの操作に慣れているかもしれません。Swift の同時実行モデルはスレッドの上に構築されていますが、スレッドを直接操作することはありません。Swift の非同期関数は、それが実行されているスレッドをあきらめ、これにより最初の関数がブロックされている間、そのスレッドで別の非同期関数を実行できます。非同期関数が再開するとき、Swift はその関数がどのスレッドで実行されるかについて保証しません。


Swift の言語サポートを使用せずに同時実行コードを作成することは可能ですが、そのコードは読みにくくなる傾向があります。たとえば、以下のコードは写真の名前のリストをダウンロードし、そのリストの最初の写真をダウンロードして、その写真をユーザーに表示します。


  1. listPhotos(inGallery: "Summer Vacation") { photoNames in
  2. let sortedNames = photoNames.sorted( )
  3. let name = sortedNames[0]
  4. downloadPhoto(named: name) { photo in
  5. show(photo)
  6. }
  7. }


この単純なケースでも、一連の完了ハンドラーとしてコードを作成する必要があるため、入れ子にされたクロージャを作成することになります。このスタイルでは、深い入れ子になった、より複雑なコードはすぐに扱いにくくなります。


非同期関数の定義と呼び出し


非同期関数 または 非同期メソッド は、実行の途中で中断できる特別な種類の関数またはメソッドです。これは、完了するまで実行するか、エラーを throw するか、決して戻らない、通常の同期関数およびメソッドとは対照的です。非同期関数またはメソッドは、これら 3 つのいずれかを実行しますが、何かを待っているときに途中で中断することもできます。非同期関数またはメソッドの本体内で、実行を中断できるこれらの場所をそれぞれマークして下さい。


関数またはメソッドが非同期であることを示すには、throws を使用して throw する関数をマークする方法と同様に、パラメータの後の宣言で async キーワードを記述します。関数またはメソッドが値を返す場合は、戻り矢印 (->) の前に async を記述します。たとえば、ギャラリー内の写真の名前を取得する方法は以下のとおりです。


  1. func listPhotos(inGallery name: String) async -> [String] {
  2. let result = // ... some asynchronous networking code ...
  3. return result
  4. }


非同期で throw の両方をする関数またはメソッドの場合、throws の前に async を記述します。


非同期メソッドを呼び出すと、そのメソッドが戻るまで実行が中断されます。呼び出しの前に await を記述して、可能な中断ポイントをマークします。これは、throw する関数を呼び出すときに try を記述して、エラーが発生した場合にプログラムの流れが変更される可能性があることをマークする事に似ています。非同期メソッド内では、別の非同期メソッドを呼び出した場合に のみ 実行の流れが中断されます — 中断が暗黙的または先制することは決してありません — つまり、考えられるすべての中断ポイントが await でマークされます。あなたのコード内で考えられる中断ポイントをすべてマークすると、同時実行コードが読みやすく、理解しやすくなります。


たとえば、以下のコードは、ギャラリー内のすべての写真の名前を取得してから、最初の写真を表示します。


  1. let photoNames = await listPhotos(inGallery: "Summer Vacation")
  2. let sortedNames = photoNames.sorted( )
  3. let name = sortedNames[0]
  4. let photo = await downloadPhoto(named: name)
  5. show(photo)


listPhotos(inGallery:)downloadPhoto(named:) 関数は両方ともネットワークリクエストを行う必要があるため、完了するまでに比較的長い時間がかかる場合があります。戻り矢印の前に async を記述して両方を非同期にすることで、このコードが写真の準備が整うまで待機している間、アプリの残りのコードを実行し続けることができます。


上記の例の同時実行の性質を理解するために、可能な実行順序の 1 つを以下に示します。


  1. コードは最初の行から実行を開始し、最初の await まで実行します。listPhotos(inGallery:) 関数を呼び出し、その関数が戻るのを待つ間、実行を中断します。

  2. このコードの実行が中断されている間、同じプログラム内の他の同時実行コードが実行されます。たとえば、長時間実行されるバックグラウンドのタスクが、新しい写真ギャラリーのリストを更新し続ける場合があります。そのコードはまた、await でマークされた次の中断ポイントまで、または完了するまで実行されます。

  3. listPhotos(inGallery:) が戻った後、このコードはその点から実行を継続します。返された値を photoNames に代入します。

  4. sortedNamesname を定義する行は、通常の同期コードです。これらの行には何も await とマークされていないため、可能な中断ポイントは全くありません。

  5. 次の await は、downloadPhoto(named:) 関数の呼び出しをマークします。このコードは、その関数が戻るまで実行を再び中断し、他の同時実行コードに実行する機会を与えます。

  6. downloadPhoto(named:) が返された後、その戻り値が photo に代入され、show(_:) を呼び出すときに引数として渡されます。

await でマークされたコード内の可能な中断ポイントは、非同期関数またはメソッドが戻るのを待っている間に、現在のコードの一部が実行を中断する可能性があることを示しています。これは、舞台裏で Swift が現在のスレッドでのコードの実行を中断し、代わりにそのスレッド上で他のコードを実行するため、スレッドの譲歩 とも呼ばれます。await を含むコードは実行を中断できる必要があるため、プログラム内の特定の場所でのみ非同期関数またはメソッドを呼び出すことができます。


  • 非同期関数、メソッド、またはプロパティの本体内のコード。

  • @main でマークされた構造体、クラス、または列挙型の static main( ) メソッド内のコード。

  • 以下の 構造化されていない同時実行 に示すように、構造化されていない子タスク内のコード。

  • Task.sleep(for:tolerance: Clock:) メソッドは、同時実行の仕組みを学ぶために簡単なコードを作成する場合に役立ちます。このメソッドは、少なくとも与えられた時間、現在のタスクを一時停止します。以下は、sleep(for:tolerance: Clock:) を使用してネットワーク操作の待機をシミュレートする listPhotos(inGallery:) 関数のバージョンです。


    1. func listPhotos(inGallery name: String) async throws -> [String] {
    2. try await Task.sleep(for: .seconds(2))
    3. return ["IMG001", "IMG99", "IMG0404"]
    4. }


    上記のコードの listPhotos(inGallery:) のバージョンは、Task.sleep(until:tolerance: Clock:) への呼び出しでエラーが throw される可能性があるため、非同期かつスローの両方をします。このバージョンの listPhotos(inGallery:) を呼び出すときは、tryawait の両方を記述します。


    let photos = try await listPhotos(inGallery: "A Rainy Weekend")


    非同期関数は throw する関数といくつかの類似点があります。非同期関数または throw する関数を定義するときは、それを async または throws でマークし、その関数への呼び出しを await または try でマークします。非同期関数は、throw する関数が別の throw する関数を呼び出せるのと同じように、別の非同期関数を呼び出せます。


    ただし、非常に重要な違いがあります。do-catch ブロックで throw するコードを包み込んてエラーを処理したり、Result を使用してコードのエラーを別の場所に保存して処理したりできます。これらのアプローチにより、throw しないコードから throw する関数を呼び出せます。例えば:


    1. func availableRainyWeekendPhotos() -> Result<[String], Error> {
    2. return Result {
    3. try listDownloadedPhotos(inGallery: "A Rainy Weekend")
    4. }
    5. }


    対照的に、同期コードから呼び出して結果を待つことができるように非同期コードを包み込める安全な方法はありません。Swift 標準ライブラリは、この安全でない機能を意図的に省略しています。これを自分で実装しようとすると、微妙な競合、スレッドの問題、およびデッドロックなどの問題が発生する可能性があります。既存のプロジェクトに同時実行コードを追加する場合は、上から下に作業します。具体的には、同時実行を使用するようにコードの最上位層を変換することから始め、次に、プロジェクトのアーキテクチャを一度に 1 層ずつ処理しながら、その層が呼び出す関数とメソッドの変換を開始します。同期コードは非同期コードを呼び出すことが全くできないため、ボトムアップアプローチを取る方法はありません。


    非同期シーケンス


    前のセクションの listPhotos(inGallery:) 関数は、配列のすべての要素の準備が整った後、一度に配列全体を非同期的に返します。もう 1 つの方法は、非同期シーケンス を使用して、一度に 1 つのコレクションの要素を待機することです。非同期シーケンスの反復処理は以下のようになります。


    1. import Foundation
    2. let handle = FileHandle.standardInput
    3. for try await line in handle.bytes.lines {
    4. print(line)
    5. }


    上記の例では、通常の for-in ループを使用する代わりに、for とその後に await を記述しています。非同期の関数またはメソッドを呼び出す場合と同様に、await を記述すると中断ポイントの可能性が示されます。for-await-in ループは、次の要素が利用可能になるのを待っているときに、各反復の開始時に実行を中断する可能性があります。


    Sequence プロトコルに準拠を追加することによって for-in ループで独自の型を使用できるのと同じ方法で、AsyncSequence プロトコルに準拠を追加することによって for-await-in ループで独自の型を使用できます。


    並列での非同期関数の呼び出し


    await を使用して非同期関数を呼び出すと、一度に 1 つのコードしか実行されません。非同期コードの実行中、呼び出し元はそのコードが終了するのを待ってから、次のコード行の実行に移ります。たとえば、ギャラリーから最初の 3 枚の写真を取得するには、以下のように downloadPhoto(named:) 関数への 3 回の呼び出しを待機 (await) できます。


    1. let firstPhoto = await downloadPhoto(named: photoNames[0])
    2. let secondPhoto = await downloadPhoto(named: photoNames[1])
    3. let thirdPhoto = await downloadPhoto(named: photoNames[2])
    4. let photos = [firstPhoto, secondPhoto, thirdPhoto]
    5. show(photos)


    このアプローチには重大な欠点があります。ダウンロードは非同期であり、進行中に他の作業を実行できますが、downloadPhoto(named:) への呼び出しは一度に 1 つしか実行されません。次の写真がダウンロードを開始する前に、各写真が完全にダウンロードされます。ただし、これらの操作を待つ必要はありません。各写真を独立してダウンロードすることも、同時にダウンロードすることもできます。


    非同期関数を呼び出して、その周りのコードと並行して実行するには、定数を定義するときに let の前に async を記述し、定数を使用するたびに await を記述して下さい。


    1. async let firstPhoto = downloadPhoto(named: photoNames[0])
    2. async let secondPhoto = downloadPhoto(named: photoNames[1])
    3. async let thirdPhoto = downloadPhoto(named: photoNames[2])
    4. let photos = await [firstPhoto, secondPhoto, thirdPhoto]
    5. show(photos)


    この例では、 downloadPhoto(named:) への 3 つの呼び出しはすべて、前の呼び出しが完了するのを待たずに開始されます。利用可能なシステムリソースが十分にある場合は、同時に実行できます。コードは関数の結果を待機するのに中断しないため、これらの関数呼び出しはいずれも await でマークされていません。代わりに、photos が定義されている行まで実行が続きます。その時点で、プログラムはこれらの非同期呼び出しの結果を必要とするため、3 つの写真すべてのダウンロードが完了するまで実行を一時停止する await を記述して下さい。


    これら 2 つのアプローチの違いについて、以下のように考えることができます。


  • 次の行のコードがその関数の結果に依存する場合は、await を使用して非同期関数を呼び出します。これにより、順次実行される作業が作成されます。

  • コードの後半まで結果が必要ない場合は、async-let を使用して非同期関数を呼び出します。これにより、並行して実行できる作業が作成されます。

  • awaitasync-let はどちらも、中断中に他のコードを実行できるようにします。

  • どちらの場合も、非同期関数が戻るまで、必要に応じて実行が中断することを示すために、可能な中断ポイントを await でマークします。

  • これらの両方のアプローチを同じコードに混在させることもできます。



    タスクとタスクグループ


    タスク は、プログラムの一部として非同期に実行できる作業単位です。すべての非同期コードは、何らかのタスクの一部として実行されます。タスク自体は一度に 1 つのことしか実行しませんが、複数のタスクを作成すると、Swift はそれらを同時に実行するようにスケジュール化できます。


    前のセクションで説明した async-let 構文により、子タスクが暗黙に作成されます。この構文は、プログラムで実行する必要があるタスクがすでにわかっている場合にうまく機能します。また、タスクグループ(TaskGroup のインスタンス)を作成し、そのグループに子タスクを明示的に追加することもできます。これにより、優先度とキャンセルをより詳細に制御でき、動的な数のタスクを作成できます。


    タスクは階層に配置されます。与えられたタスクグループ内の各タスクには同じ親タスクがあり、各タスクは子タスクを持つことができます。タスクとタスクグループの間には明示的な関係があるため、このアプローチは 構造化された同時実行性 と呼ばれます。タスク間の明示的な親子関係には、いくつかの利点があります。


  • 親タスクでは、子タスクが完了するまで待つことを忘れてはなりません。

  • 子タスクに高い優先度を設定すると、親タスクの優先度が自動的に上がります。

  • 親タスクがキャンセルされると、その子タスクもそれぞれ自動的にキャンセルされます。

  • タスクローカル値は、子タスクに効率的かつ自動的に伝播されます。

  • 以下は、任意の数の写真を処理する、写真をダウンロードするコードの別のバージョンです。


    1. await withTaskGroup(of: Data.self) { group in
    2. let photoNames = await listPhotos(inGallery: Summer Vacation")
    3. for name in photoNames {
    4. group.addTask {
    5. return await downloadPhoto(named: name) }
    6. }
    7. }
    8. for await photo in group {
    9. show(photo)
    10. }
    11. }


    上記のコードは、新しいタスク グループを作成し、ギャラリー内の各写真をダウンロードするための子タスクを作成します。Swift は、条件が許す限りこれらのタスクをできるだけ多く同時に実行します。子タスクが写真のダウンロードを完了するとすぐに、その写真が表示されます。子タスクが完了する順序については保証がないため、このギャラリーの写真は任意の順序で表示されます。


    注意: 写真をダウンロードするコードがエラーを throw する可能性がある場合は、代わりに withThrowingTaskGroup(of:returning:body:) を呼び出して下さい。


    上記のコードリストでは、各写真がダウンロードされてから表示されるため、タスクグループは結果を全く返しません。結果を返すタスクグループの場合は、withTaskGroup(of:returning:body:) に渡すクロージャ内に結果を蓄積するコードを追加して下さい。


    1. let photos = await withTaskGroup(of: Data.self) { group in
    2. let photoNames = await listPhotos(inGallery: "Summer Vacation")
    3. for name in photoNames {
    4. group.addTask {
    5. return await downloadPhoto(named: name)
    6. }
    7. }
    8. var results: [Data] = []
    9. for await photo in group {
    10. results.append(photo)
    11. }
    12. return results
    13. }


    前の例と同様に、この例では写真ごとに子タスクを作成してダウンロードします。 前の例とは異なり、for-await-in ループは次の子タスクが終了するのを待ち、そのタスクの結果を結果の配列に追加して、すべての子タスクが終了するまで待機し続けます。最後に、タスクグループは、ダウンロードされた写真の配列を全体的な結果として返します。


    タスクのキャンセル


    Swift の同時実行では、協調キャンセルモデルが使用されます。各タスクは、実行中の適切な時点でキャンセルされたかどうかを確認し、キャンセルに適切に応答します。タスクが実行している作業に応じて、キャンセルへの応答は通常、次のいずれかを意味します。


  • CancellationError のようなエラーを throw する

  • nil または空のコレクションを返す

  • 途中まで完成した作業を返す

  • 写真が大きい場合やネットワークが遅い場合は、写真のダウンロードに時間がかかります。すべてのタスクが完了するのを待たずにユーザーがこの作業を停止できるようにするには、タスクがキャンセルされているかどうかを確認し、キャンセルされている場合は実行を停止する必要があります。タスクでこれを行うには 2 つの方法があります。Task.checkCancellation() 型メソッドを呼び出すことと、Task.isCancelled 型プロパティを読み取ることです。タスクがキャンセルされた場合、checkCancellation() を呼び出すとエラーが throws されます。throw するタスクは、タスクの外にエラーを伝播し、タスクのすべての作業を停止する可能性があります。これには、実装と理解が簡単であるという利点があります。柔軟性を高めるには、isCancelled プロパティを使用します。これにより、ネットワーク接続の終了や一時ファイルの削除など、タスクの停止の一部としてクリーンアップ作業を実行できます。


    1. let photos = await withTaskGroup(of: Optional<Data>.self) { group in
    2. let photoNames = await listPhotos(inGallery: "Summer Vacation")
    3. for name in photoNames {
    4. let added = group.addTaskUnlessCancelled {
    5. Task.isCancelled ? nil : await downloadPhoto(named: name)
    6. }
    7. guard added else { break }
    8. }
    9. var results: [Data] = []
    10. for await photo in group {
    11. if let photo { results.append(photo) }
    12. }
    13. return results
    14. }


    上記のコードには、以前のバージョンからいくつかの変更が加えられています。


  • 各タスクは、キャンセル後に新しい作業が開始されることを避けるために、TaskGroup.addTaskUnlessCancelled(priority:operation:) メソッドを使用して追加されます。

  • addTaskUnlessCancelled(priority:operation:) への各呼び出しの後に、コードは新しい子タスクが追加されたことを確認します。グループがキャンセルされた場合、added の値は false になります。その場合、コードは追加の写真のダウンロードの試みを中止します。

  • 各タスクは、写真のダウンロードを開始する前にキャンセルを確認します。キャンセルされた場合、タスクは nil を返します。

  • 最後に、タスクグループは結果を収集するときに nil 値をスキップします。nil を返してキャンセルを処理するということは、タスクグループが完了した作業を破棄する代わりに、部分的な結果 (キャンセル時に既にダウンロードされていた写真) を返すことができることを意味します。

  • 注意: タスクがそのタスクの外部からキャンセルされたかどうかを確認するには、type プロパティの代わりに Task.isCancelled インスタンスプロパティを使用します。

    キャンセルの即時通知が必要な作業の場合は、Task.withTaskCancellationHandler(operation:onCancel:) メソッドを使用します。例えば:


    1. let task = await Task.withTaskCancellationHandler {
    2. // ...
    3. } onCancel: {
    4. print("Canceled!")
    5. }
    6. // ... some time later...
    7. task.cancel() // Prints "Canceled!"


    キャンセルハンドラーを使用する場合でも、タスクのキャンセルはまだ協調的です。タスクは完了するまで実行されるか、キャンセルを確認して早期に停止します。キャンセルハンドラーの開始時にタスクはまだ実行中であるため、タスクとそのキャンセルハンドラーの間で状態を共有しないようにしてください。競合状態が発生する可能性があります。


    構造化されていない同時実行


    前のセクションで説明した同時実行に対する構造化されたアプローチに加えて、Swift はまた構造化されていない同時実行もサポートしています。タスクグループの一部であるタスクとは異なり、構造化されていないタスク には親タスクがありません。プログラムが必要とするどんな方法でも構造化されていないタスクを管理するには完全な柔軟性があなたにありますが、それらの正確性についても完全に責任があります。


    周囲のコードと同様の構造化されていないタスクを作成するには、Task.init(priority:operation:) イニシャライザを呼び出します。新しいタスクは、デフォルトで現在のタスクと同じアクター分離、優先度、およびタスクローカル状態で実行されます。周囲のコードから、より独立した非構造化タスク(より具体的には デタッチタスク)を作成するには、静的メソッド Task.detached(name:priority:operation:) を呼び出します。新しいタスクはデフォルトでアクター分離なしで実行され、現在のタスクの優先度やタスクローカル状態を継承しません。これらの操作はどちらも、例えば結果を待機したりキャンセルしたりするなど、ユーザーが操作できるタスクを返します。


    1. let newPhoto = // ... some photo data ...
    2. let handle = Task {
    3. return await add(newPhoto, toGalleryNamed: "Spring Adventures")
    4. }
    5. let result = await handle.value

    分離タスクの管理の詳細については、Task を参照してください。



    分離


    前のセクションでは、同時実行作業を分割する方法について説明しました。これらの作業には、アプリの UI などの共有データの変更が含まれます。あなたのコードの複数の部分から同時に同じデータが変更されると、データ競合が発生するリスクがあります。Swift は、コード内のデータ競合を防ぎます。Swift は、データを読み込んだり変更したりするたびに、他のコードが同時にそのデータを変更していないことを保証します。この保証は データ分離 と呼ばれます。データを分離するには、主に 3 つの方法があります。


    1. 不変データは常に分離されています。定数は変更できないため、定数を読み込んでいる最中に他のコードが定数を変更するリスクはありません。

    2. 現在のタスクのみが参照するデータは常に分離されています。ローカル変数は、タスク外部のコードがそのメモリを参照していないため、他のコードがそのデータを変更できないので、安全に読み書きできます。さらに、変数をクロージャでキャプチャした場合、Swift はそのクロージャが同時に使用されないようにします。

    3. アクターによって保護されているデータは、そのデータにアクセスするコードもアクターに分離されている場合、分離されています。現在の関数がアクターに分離されている場合、そのアクターによって保護されているデータの読み取りと書き込みは安全です。なぜなら、同じアクターに分離された他のコードは、実行前にその順番を待たなければならないからです。


    メインアクター(Main Actor)


    アクターとは、コードが順番にそのデータにアクセスするのを強制することで、可変データへのアクセスを保護するオブジェクトです。多くのプログラムで最も重要なアクターは メインアクター です。アプリでは、メインアクターが UI の表示に使用されるすべてのデータを保護します。メインアクターは、あなたの記載する UI のレンダリング、UI イベントの処理、UI の照会や更新を必要とするコードの実行を順番に行います。


    コードで並行処理を使用する前は、すべてメインアクター上で実行されます。長時間実行されるコードやリソースを大量に消費するコードを特定したら、安全かつ正確な方法でこれらの処理をメインアクターから移動できます。


    注意: メインアクターはメインスレッドと密接に関連していますが、同じものではありません。メインアクターはプライベートで可変的な状態を持ち、メインスレッドはその状態へのアクセスを連続化します。メインアクターでコードを実行すると、Swift はそのコードをメインスレッドで実行します。この関連性から、これら 2 つの用語は交換可能に使われることがあります。コードはメインアクターとやり取りしますが、メインスレッドはより低レベルの実装の詳細です。

    メインアクターで処理を実行する方法はいくつかあります。関数が常にメインアクターで実行されるようにするには、@MainActor 属性でマークします。


    1. @MainActor
    2. func show(_: Data) {
    3. // ... UI code to display the photo ...
    4. }

    上記のコードでは、 show(_:) 関数の @MainActor 属性により、この関数はメインアクターでのみ実行されることが要求されています。メインアクターで実行されている他のコード内では、 show(_:) を同期関数として呼び出すことができます。ただし、メインアクターで実行されていないコードから show(_:) を呼び出すには、await を記述して非同期関数としてそれを呼び出す必要があります。これは、メインアクターへの切り替えによって潜在的な中断ポイントが発生するためです。例えば、次のようになります。


    1. func downloadAndShowPhoto(named name: String) async {
    2. let photo = await downloadPhoto(named: name)
    3. await show(photo)
    4. }

    上記のコードでは、downloadPhoto(named:) 関数と show(_:) 関数の両方が、呼び出し時に一時停止する可能性があります。このコードにはまた、一般的なパターンも示されています。つまり、長時間実行され CPU を大量に使用する作業をバックグラウンドで実行し、その後、メインアクターに切り替えて UI を更新します。downloadAndShowPhoto(named:) 関数はメインアクター上にないため、downloadPhoto(named:) 内の作業もメインアクター上では実行されません。UI を更新する show(_:) 内の作業のみがメインアクター上で実行されます。これは、この関数に @MainActor 属性が付与されているためです。


    クロージャがメインアクター上で実行されるようにするには、クロージャの先頭で、キャプチャリストの前、かつ in の前に @MainActor を記述します。


    1. let photo = await downloadPhoto(named: "Trees at Sunrise")
    2. Task { @MainActor in
    3. show(photo)
    4. }

    上記のコードは、前のコードリストの downloadAndShowPhoto(named:) と似ていますが、この例のコードでは UI の更新を待ちません。構造体、クラス、または列挙体にも @MainActor を記述することで、そのすべてのメソッドとプロパティへのアクセスがメインアクター上で実行されるようにすることができます。


    1. @MainActor
    2. struct PhotoGallery {
    3. var photoNames: [String]
    4. func drawUI() { /* ... other UI code ... */ }
    5. }

    上記のコード内の PhotoGallery 構造体は、画面に写真を描画する際に、その photoNames プロパティの名前を使用して表示する写真を決定します。photoNames は UI に影響を与えるため、それを変更するコードはメインアクター上で実行し、アクセスを連続化する必要があります。


    フレームワーク上に構築する場合、そのフレームワークのプロトコルと基底クラスは通常既に @MainActor としてマークされているため、その場合は独自の型に @MainActor を記述することはあまりありません。以下に簡略化した例を示します。


    1. @MainActor
    2. protocol View { /* ... */ }
    3. // Implicitly @MainActor
    4. struct PhotoGalleryView: View { /* ... */ }

    上記のコードでは、SwiftUI のようなフレームワークが View プロトコルを定義しています。プロトコル宣言に @MainActor を記述することで、このプロトコルに準拠する PhotoGalleryView などの型も暗黙的に @MainActor としてマークされます。View が基底クラスで PhotoGalleryView がサブクラスの場合も同様の動作となり、サブクラスは暗黙的に @MainActor としてマークされます。


    上記の例では、PhotoGallery はメインアクター上の構造体全体を保護しています。よりきめ細かな制御を行うには、メインスレッド上でアクセスまたは実行する必要があるプロパティやメソッドのみに @MainActor を記述できます。


    1. struct PhotoGallery {
    2. @MainActor var photoNames: [String]
    3. var hasCachedPhotos = false
    4. @MainActor func drawUI() { /* ... UI code ... */ }
    5. func cachePhotos() { /* ... networking code ... */ }
    6. }

    上記の PhotoGallery のバージョンでは、drawUI() メソッドはギャラリーの写真を画面に描画するため、メインアクターに分離する必要があります。photoNames プロパティは UI を直接作成しませんが、drawUI() 関数が UI を描画するために使用する状態を保存するため、このプロパティもメインアクター上でのみアクセスする必要があります。一方、hasCachedPhotos プロパティへの変更は UI に影響を与えず、cachePhotos() メソッドはメインアクターでの実行を必要とする状態にアクセスしません。そのため、これらは @MainActor でマークがされていません。


    以前の例と同様に、UI の構築にフレームワークを使用している場合、そのフレームワークのプロパティラッパーによって、UI 状態プロパティが既に @MainActor としてマークされている可能性があります。プロパティラッパーを定義する際に、その wrappedValue プロパティが @MainActor としてマークされている場合、そのプロパティラッパーを適用するすべてのプロパティも暗黙的に @MainActor としてマークされます。



    アクター(Actors)


    Swift はメインアクターを提供していますが、あなた独自のアクターを定義することもできます。アクターを使うと、同時実行するコード間で安全に情報を共有できます。


    クラスと同様に、アクターは参照型であるため、クラスは参照型 での値型と参照型の比較は、クラスだけでなくアクターにも適用されます。クラスとは異なり、アクターは、一度に 1 つのタスクのみが可変状態にアクセスできるため、複数のタスクでのコードがアクターの同じインスタンスを操作しても安全です。たとえば、気温を記録するアクターは以下のとおりです。


    1. actor TemperatureLogger {
    2. let label: String
    3. var measurements: [Int]
    4. private(set) var max: Int
    5. init(label: String, measurement: Int) {
    6. self.label = label
    7. self.measurements = [measurement]
    8. self.max = measurement
    9. }
    10. }


    actor キーワードを使用してアクターを導入し、その後にその定義を中かっこのペアで囲みます。TemperatureLogger アクターには、アクターの外の他のコードがアクセスできるプロパティがあり、アクター内のコードのみが最大値を更新できるように max プロパティを制限します。


    構造体やクラスと同じ初期化構文を使用して、アクターのインスタンスを作成して下さい。アクターのプロパティまたはメソッドにアクセスするときは、await を使用して潜在的な中断ポイントをマークして下さい。例えば:


    1. let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
    2. print(await logger.max)
    3. // Prints "25"


    この例では、logger.max へのアクセスが中断ポイントになる可能性があります。アクターは一度に 1 つのタスクのみが可変状態にアクセスできるため、別のタスクのコードが既にロガーを操作している場合、このコードはプロパティへのアクセスを待機している間中断されます。


    対照的に、アクターの一部であるコードは、アクターのプロパティにアクセスするときに await を書き込みません。たとえば、TemperatureLogger を新しい温度で更新するメソッドは以下のとおりです。


    1. extension TemperatureLogger {
    2. func update(with measurement: Int) {
    3. measurements.append(measurement)
    4. if measurement > max {
    5. max = measurement
    6. }
    7. }
    8. }


    update(with:) メソッドはすでにアクター上で実行されているため、max などのプロパティへのアクセスを await でマークしません。このメソッドはまた、アクターが一度に 1 つのタスクしか可変状態を操作させない理由の 1 つも示しています。アクターの状態の何らかの更新によって、一時的に不変条件が壊れます。TemperatureLogger のアクターは、温度と最高温度のリストを追跡し、新しい測定値を記録すると最高温度を更新します。更新の途中で、新しい測定値を追加した後、max を更新する前に、温度ロガーが一時的に矛盾した状態になります。複数のタスクが同じインスタンスを同時に操作しないようにすることで、以下の一連のイベントのような問題を防ぐことができます。


    1. コードは update(with:) メソッドを呼び出します。最初に measurement 配列を更新します。

    2. コードが max を更新できる前に、他の場所のコードが最大値と温度の配列を読み取ります。

    3. コードは max を変更して更新を終了します。

    この場合、アクターへのアクセスが update(with:) への呼び出しの途中でインターリーブされ、データが一時的に無効になるため、別の場所で実行されているコードが誤った情報を読み取ることになります。Swift のアクターを使用すると、それらの状態には一度に 1 つの操作しか許可されず、そのコードは await が中断ポイントをマークする場所でのみ中断できるため、この問題を回避できます。update(with:) には中断ポイントが含まれていないため、他のコードは更新中にはデータにアクセスできません。


    アクターの外部のコードが構造体やクラスのプロパティにアクセスするなど、これらのプロパティに直接アクセスしようとすると、コンパイル時エラーが発生します。例えば:


    print(logger.max) // Error



    アクターのプロパティはそのアクターの分離されたローカル状態の一部であるため、await を書き込まずに logger.max にアクセスすると失敗します。このプロパティにアクセスするコードはアクターの一部として実行する必要があります。これは非同期操作であり、await を記述する必要があります。Swift は、アクター上で実行されているコードのみがそのアクターのローカル状態にアクセスできることを保証します。この保証は アクター分離 として知られています。


    Swift の同時実行モデルの以下の側面が連携して、共有の可変状態についての推論を容易にします。


  • 可能な中断ポイント間のコードは、他の同時実行コードから中断されることなく、順次実行されます。ただし、複数の同時実行コードを同時に実行できるため、他のコードも同時に実行される可能性があります。

  • アクターのローカル状態を操作するコードは、そのアクター上でのみ実行されます。

  • アクターは一度に 1 つのコードのみを実行します。

  • これらの保証により、await を含まず、アクター内にあるコードは、プログラム内の他の場所で一時的に無効な状態が観察されるリスクを負うことなく、更新を行うことができます。たとえば、以下のコードは測定温度を華氏から摂氏に変換します。


    1. extension TemperatureLogger {
    2. func convertFahrenheitToCelsius( ) {
    3. for i in measurements.indices {
    4. measurements[i] = (measurements[i] - 32) * 5 / 9
    5. }
    6. }
    7. }

    上記のコードは、測定値の配列を一度に 1 つずつ変換します。マップ操作の進行中、一部の温度は華氏で表示され、その他は摂氏で表示されます。ただし、どのコードにも await が含まれていないため、このメソッドには潜在的な中断ポイントがありません。このメソッドが変更する状態はアクターに属し、そのコードがアクター上で実行される場合を除き、コードの読み取りや変更からアクターを保護します。これは、単位変換の進行中に、他のコードが部分的に変換された温度のリストを読み取る方法がないことを意味します。


    潜在的な中断ポイントを省略して一時的に無効な状態を保護するコードをアクターで書き込むだけでなく、そのコードを同期メソッドに移動することもできます。上記の convertFahrenheitToCelsius( ) メソッドは同期メソッドであるため、潜在的な中断ポイントが 決して 含まれないことが保証されています。この関数は、データモデルの一貫性を一時的に失わせるコードをカプセル化し、作業を完了してデータの一貫性が回復するまでは他のコードを実行できないことを、コードを読む人が容易に認識できるようにします。将来、この関数に同時実行コードを追加して中断ポイントが発生する可能性がある場合、バグが発生する代わりにコンパイル時エラーが発生するでしょう。



    グローバルアクター(Global Actors)


    メインアクターは、MainActor 型のグローバルシングルトンのインスタンスです。アクターは通常、複数のインスタンスを持つことができ、それぞれが独立した分離性を提供します。そのため、アクターのすべての分離データは、そのアクターのインスタンスプロパティとして宣言します。しかし、MainActor はシングルトン(この型のインスタンスは常に 1 つだけ)であるため、型だけでアクターを識別でき、属性だけでメインアクターの分離を示すことができます。このアプローチにより、コードをより柔軟にして、自分に最適な方法で構成できるようになります。


    globalActor で説明されているように、@globalActor 属性を使用して独自のシングルトングローバルのアクターを定義できます。



    Sendable な型


    タスクとアクターを使用するとプログラムを、安全に同時実行できる部分に分割できます。タスクまたはアクターのインスタンス内で、可変状態 (変数やプロパティなど) を含むプログラムの部分は、同時実行ドメイン と呼ばれます。データには変更可能な状態が含まれているため、一部の種類のデータは同時実行ドメイン間で共有できませんが、アクセスの重複を防ぐことはできません。


    ある同時実行ドメインから別のドメインに共有できる型は、Sendable な型 と呼ばれます。たとえば、アクターメソッドを呼び出すときに引数として渡すか、タスクの結果として返すことができます。この章の前半の例では、同時実行ドメイン間で渡されるデータを常に安全に共有できる単純な値型を使用しているため、送信可能性 (sendability) については説明しませんでした。対照的に、一部の型では同時実行ドメイン間で安全に渡すことができません。たとえば、変更可能なプロパティを含み、それらのプロパティへのアクセスを順次化しないクラスは、異なるタスク間でそのクラスのインスタンスを渡すときに、予測できない誤った結果を生成する可能性があります。


    Sendable プロトコルへの準拠を宣言することにより、型を sendable (送信可能) としてマークして下さい。そのプロトコルにはコード要件はありませんが、Swift が強制する意味要件があります。一般に、型を送信可能 (sendable) にする方法は 3 つあります。


  • 型は値型であり、その変更可能な状態は他の送信可能なデータで構成されています。たとえば、送信可能な格納されたプロパティを持つ構造体や、送信可能な関連する値を持つ列挙型などです。

  • 型には変更可能な状態はなく、その不変な状態は他の送信可能なデータ (たとえば、読み取り専用プロパティのみを持つ構造体またはクラス) で構成されています。

  • 型には、@MainActor とマークされたクラスや、特定のスレッドまたはキューでそのプロパティへのアクセスを順次化するクラスなど、その可変状態の安全性を保証するコードがあります。

  • 意味的要件の詳細なリストについては、Sendable プロトコルリファレンスを参照してください。


    送信可能 (sendable) なプロパティのみを持つ構造体や、送信可能な関連する値のみを持つ列挙型など、一部の型は常に送信可能です。例えば:


    1. struct TemperatureReading: Sendable {
    2. var measurement: Int
    3. }
    4. extension TemperatureLogger {
    5. func addReading(from reading: TemperatureReading) {
    6. measurements.append(reading.measurement)
    7. }
    8. }
    9. let logger = TemperatureLogger(label: "Tea kettle", measurement: 85)
    10. let reading = TemperatureReading(measurement: 45)
    11. await logger.addReading(from: reading)


    TemperatureReading は送信可能 (sendable) なプロパティのみを持つ構造体であり、この構造体は public または @usableFromInline としてマークされていないため、暗黙的に送信可能 (sendable) です。Sendable プロトコルへの準拠が暗示される構造体のバージョンを以下に示します。


    1. struct TemperatureReading {
    2. var measurement: Int
    3. }


    型を送信不可として明示的にマークするには、Sendable に準拠不可能と記述します。


    1. struct FileDescriptor {
    2. let rawValue: Int
    3. }
    4. @available(*, unavailable)
    5. extension FileDescriptor: Sendable { }


    プロトコルへの暗黙的な準拠 で説明されているように、利用できない準拠を使用してプロトコルへの暗黙的な準拠を抑制することもできます。


    前:エラー処理 次:マクロ
















    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ












    トップへ