並行処理
非同期操作を実行します。
Swiftには、構造化された方法で非同期および並列コードを書くための組み込みサポートがあります。非同期コードは一時停止して後で再開することができますが、一度にプログラムの一部しか実行されません。プログラム内のコードを一時停止して再開することで、UIの更新などの短期的な操作を続けながら、ネットワーク経由でデータを取得したりファイルを解析したりするような長期的な操作を続けることができます。並列コードとは、複数のコードが同時に実行されることを意味します。例えば、4コアのプロセッサを持つコンピュータは、4つのコードを同時に実行でき、それぞれのコアが1つのタスクを実行します。並列および非同期コードを使用するプログラムは、複数の操作を同時に実行し、外部システムを待っている操作を一時停止します。
並列または非同期コードによる追加のスケジューリング柔軟性には、複雑さの増加というコストも伴います。Swiftでは、アクターを使用して可変状態に安全にアクセスするなど、コンパイル時のチェックを可能にする方法で意図を表現できます。しかし、遅いまたはバグのあるコードに並行処理を追加しても、それが高速または正確になる保証はありません。実際、並行処理を追加すると、コードのデバッグが難しくなることさえあります。しかし、並行処理が必要なコードにSwiftの言語レベルのサポートを使用することで、Swiftはコンパイル時に問題をキャッチするのに役立ちます。
この章の残りの部分では、非同期および並列コードの一般的な組み合わせを指すために「並行処理」という用語を使用します。
注: 以前に並行コードを書いたことがある場合、スレッドを使用して作業することに慣れているかもしれません。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)
}
}
この単純なケースでも、コードを一連の完了ハンドラとして書く必要があるため、ネストされたクロージャを書くことになります。このスタイルでは、深いネストを持つより複雑なコードはすぐに扱いにくくなります。
非同期関数の定義と呼び出し
非同期関数または非同期メソッドは、実行中に一時停止できる特別な種類の関数またはメソッドです。これは、完了するまで実行されるか、エラーをスローするか、または決して戻らない通常の同期関数およびメソッドとは対照的です。非同期関数またはメソッドもこれらの3つのいずれかを行いますが、何かを待っている間に途中で一時停止することもできます。非同期関数またはメソッドの本体内では、実行が一時停止できる場所をそれぞれマークします。
関数またはメソッドが非同期であることを示すには、パラメータの後にasync
キーワードを宣言に書きます。これは、スロー関数をマークするためにthrows
を使用する方法に似ています。関数またはメソッドが値を返す場合、戻り矢印(->
)の前にasync
を書きます。例えば、ギャラリー内の写真の名前を取得する方法は次のようになります:
func listPhotos(inGallery name: String) async -> [String] {
let result = // ... 一部の非同期ネットワーキングコード ...
return result
}
非同期かつスローする関数またはメソッドの場合、throws
の前にasync
を書きます。
非同期メソッドを呼び出すと、メソッドが戻るまで実行が一時停止します。可能な一時停止ポイントをマークするために、呼び出しの前にawait
を書きます。これは、スロー関数を呼び出すときに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
を書くことで、これらの関数を非同期にすることで、このコードが写真の準備が整うのを待っている間にアプリの他のコードが実行され続けることができます。
上記の例の並行性の性質を理解するために、次のような実行順序の一例を示します:
- コードは最初の行から実行を開始し、最初の
await
まで実行されます。listPhotos(inGallery:)
関数を呼び出し、その関数が戻るのを待っている間、実行が一時停止します。 - このコードの実行が一時停止している間、同じプログラム内の他の並行コードが実行されます。例えば、長時間実行されるバックグラウンドタスクが新しい写真ギャラリーのリストを更新し続けるかもしれません。そのコードも次の一時停止ポイント(
await
でマークされている)まで、または完了するまで実行されます。 listPhotos(inGallery:)
が戻った後、このコードはそのポイントから実行を再開します。返された値がphotoNames
に割り当てられます。sortedNames
およびname
を定義する行は通常の同期コードです。これらの行にはawait
がマークされていないため、可能な一時停止ポイントはありません。- 次の
await
はdownloadPhoto(named:)
関数の呼び出しをマークします。このコードはその関数が戻るまで再び実行を一時停止し、他の並行コードが実行される機会を与えます。 downloadPhoto(named:)
が戻った後、その戻り値がphoto
に割り当てられ、次にshow(_:)
を呼び出すときに引数として渡されます。
コード内の await
でマークされた可能性のある中断ポイントは、非同期関数やメソッドが戻るのを待っている間に現在のコードの実行が一時停止する可能性があることを示しています。これはスレッドの譲渡とも呼ばれます。なぜなら、Swiftはコードの実行を現在のスレッドで中断し、そのスレッドで他のコードを実行するからです。await
を含むコードは実行を中断できる必要があるため、プログラム内の特定の場所でのみ非同期関数やメソッドを呼び出すことができます。
- 非同期関数、メソッド、またはプロパティの本体内のコード。
@main
でマークされた構造体、クラス、または列挙の静的main()
メソッド内のコード。- 下記の「非構造化並行処理」で示されるような非構造化子タスク内のコード。
Task.yield()
メソッドを呼び出すことで、中断ポイントを明示的に挿入できます。
func generateSlideshow(forGallery gallery: String) async {
let photos = await listPhotos(inGallery: gallery)
for photo in photos {
// ...この写真のビデオを数秒間レンダリングする...
await Task.yield()
}
}
ビデオをレンダリングするコードが同期的であると仮定すると、それには中断ポイントが含まれていません。ビデオをレンダリングする作業は長時間かかる可能性があります。しかし、定期的に Task.yield()
を呼び出して明示的に中断ポイントを追加できます。このように長時間実行されるコードを構成することで、Swiftはこのタスクの進行と、プログラム内の他のタスクが作業を進めることのバランスを取ることができます。
Task.sleep(for:tolerance:clock:)
メソッドは、並行処理の仕組みを学ぶための簡単なコードを書く際に役立ちます。このメソッドは、指定された時間だけ現在のタスクを中断します。以下は、ネットワーク操作を待つことをシミュレートするために sleep(for:tolerance:clock:)
を使用する listPhotos(inGallery:)
関数のバージョンです。
func listPhotos(inGallery name: String) async throws -> [String] {
try await Task.sleep(for: .seconds(2))
return ["IMG001", "IMG99", "IMG0404"]
}
上記のコードの listPhotos(inGallery:)
バージョンは、非同期であり、かつエラーをスローします。なぜなら、Task.sleep(until:tolerance:clock:)
の呼び出しがエラーをスローする可能性があるからです。このバージョンの listPhotos(inGallery:)
を呼び出すときは、try
と await
の両方を書きます。
let photos = try await listPhotos(inGallery: "A Rainy Weekend")
非同期関数には、スロー関数といくつかの類似点があります。非同期またはスロー関数を定義するときは、それを async
または throws
でマークし、その関数の呼び出しを await
または try
でマークします。非同期関数は、他の非同期関数を呼び出すことができ、スロー関数は他のスロー関数を呼び出すことができます。
しかし、非常に重要な違いがあります。スローコードを do-catch
ブロックでラップしてエラーを処理するか、Result
を使用してエラーを他のコードで処理するために保存することができます。これらのアプローチにより、スロー関数を非スローコードから呼び出すことができます。例えば:
func availableRainyWeekendPhotos() -> Result<[String], Error> {
return Result {
try listDownloadedPhotos(inGallery: "A Rainy Weekend")
}
}
対照的に、非同期コードをラップして同期コードから呼び出し、その結果を待つ安全な方法はありません。Swift標準ライブラリは意図的にこの安全でない機能を省略しています。自分で実装しようとすると、微妙な競合、スレッドの問題、デッドロックなどの問題が発生する可能性があります。既存のプロジェクトに並行コードを追加する場合は、上から下へと作業を進めます。具体的には、最上位のコード層を並行処理を使用するように変換し、それが呼び出す関数やメソッドを変換し、プロジェクトのアーキテクチャを一層ずつ進めていきます。下から上へのアプローチを取る方法はありません。なぜなら、同期コードは非同期コードを呼び出すことができないからです。
非同期シーケンス
前のセクションの listPhotos(inGallery:)
関数は、配列のすべての要素が準備できた後に一度に全体の配列を非同期に返します。別のアプローチとして、非同期シーケンスを使用してコレクションの要素を一度に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
が定義される行まで実行が続行されます。その時点で、プログラムはこれらの非同期呼び出しの結果を必要とするため、すべての写真のダウンロードが完了するまで実行を一時停止するために await
を書きます。
これら2つのアプローチの違いについては次のように考えることができます。
- コードの後続行がその関数の結果に依存する場合は、
await
を使用して非同期関数を呼び出します。これにより、順次実行される作業が作成されます。 - 結果が後のコードまで必要ない場合は、
async-let
を使用して非同期関数を呼び出します。これにより、並行して実行できる作業が作成されます。
await
と async-let
の両方が、中断中に他のコードを実行できるようにします。
どちらの場合も、非同期関数が戻るまで必要に応じて実行が一時停止することを示すために await
で中断ポイントをマークします。
これらのアプローチを同じコード内で混在させることもできます。
タスクとタスクグループ
タスクは、プログラムの一部として非同期に実行できる作業単位です。すべての非同期コードは何らかのタスクの一部として実行されます。タスク自体は一度に一つのことしか行いませんが、複数のタスクを作成すると、Swiftはそれらを同時に実行するようにスケジュールできます。
前のセクションで説明した async-let
構文は暗黙的に子タスクを作成します。この構文は、プログラムが実行する必要があるタスクがすでにわかっている場合に適しています。また、タスクグループ(TaskGroup
のインスタンス)を作成し、そのグループに子タスクを明示的に追加することもできます。これにより、優先順位やキャンセルの制御が可能になり、動的な数のタスクを作成できます。
タスクは階層的に配置されます。特定のタスクグループ内の各タスクは同じ親タスクを持ち、各タスクは子タスクを持つことができます。タスクとタスクグループの間に明示的な関係があるため、このアプローチは構造化された並行性と呼ばれます。タスク間の明示的な親子関係にはいくつかの利点があります:
- 親タスクでは、子タスクが完了するのを待つのを忘れることはありません。
- 子タスクに高い優先順位を設定すると、親タスクの優先順位も自動的に上がります。
- 親タスクがキャンセルされると、その子タスクもすべて自動的にキャンセルされます。
- タスクローカルの値は、子タスクに効率的かつ自動的に伝播されます。
以下は、任意の数の写真をダウンロードするコードの別バージョンです:
await withTaskGroup(of: Data.self) { group in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
group.addTask {
return await downloadPhoto(named: name)
}
}
for await photo in group {
show(photo)
}
}
上記のコードは新しいタスクグループを作成し、ギャラリー内の各写真をダウンロードするための子タスクを作成します。Swiftは条件が許す限り、これらのタスクを同時に実行します。子タスクが写真のダウンロードを完了するとすぐに、その写真が表示されます。子タスクが完了する順序には保証がないため、このギャラリーの写真は任意の順序で表示される可能性があります。
注: 写真をダウンロードするコードがエラーをスローする可能性がある場合は、代わりに
withThrowingTaskGroup(of:returning:body:)
を呼び出します。
上記のコードリストでは、各写真がダウンロードされて表示されるため、タスクグループは結果を返しません。結果を返すタスクグループの場合、withTaskGroup(of:returning:body:)
に渡すクロージャ内で結果を蓄積するコードを追加します。
let photos = await withTaskGroup(of: Data.self) { group in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
group.addTask {
return await downloadPhoto(named: name)
}
}
var results: [Data] = []
for await photo in group {
results.append(photo)
}
return results
}
前の例と同様に、この例では各写真をダウンロードするために子タスクを作成します。前の例とは異なり、for-await-in
ループは次の子タスクが完了するのを待ち、そのタスクの結果を結果の配列に追加し、すべての子タスクが完了するまで待ち続けます。最後に、タスクグループはダウンロードされた写真の配列を全体の結果として返します。
タスクのキャンセル
Swiftの並行処理は協調的なキャンセルモデルを使用します。各タスクは実行の適切なポイントでキャンセルされているかどうかを確認し、適切にキャンセルに応答します。タスクが行っている作業に応じて、キャンセルに応答する方法は通常次のいずれかです:
CancellationError
のようなエラーをスローするnil
または空のコレクションを返す- 部分的に完了した作業を返す
写真のダウンロードは、写真が大きい場合やネットワークが遅い場合に長時間かかることがあります。すべてのタスクが完了するのを待たずにこの作業を停止できるようにするためには、タスクがキャンセルを確認し、キャンセルされた場合は実行を停止する必要があります。タスクがこれを行う方法は2つあります:Task.checkCancellation()
タイプメソッドを呼び出すか、Task.isCancelled
タイププロパティを読み取ることです。checkCancellation()
を呼び出すと、タスクがキャンセルされた場合にエラーがスローされます。スローするタスクはエラーをタスクの外に伝播させ、タスクのすべての作業を停止します。これには、実装と理解が簡単であるという利点があります。より柔軟性を求める場合は、isCancelled
プロパティを使用します。これにより、ネットワーク接続の終了や一時ファイルの削除など、タスクの停止の一環としてクリーンアップ作業を行うことができます。
let photos = await withTaskGroup(of: Optional<Data>.self) { group in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
let added = group.addTaskUnlessCancelled {
guard !Task.isCancelled else { return nil }
return await downloadPhoto(named: name)
}
guard added else { break }
}
var results: [Data] = []
for await photo in group {
if let photo { results.append(photo) }
}
return results
}
上記のコードは、前のバージョンからいくつかの変更を加えています:
- 各タスクは
TaskGroup.addTaskUnlessCancelled(priority:operation:)
メソッドを使用して追加され、キャンセル後に新しい作業を開始しないようにします。 addTaskUnlessCancelled(priority:operation:)
の各呼び出し後に、新しい子タスクが追加されたことを確認します。グループがキャンセルされた場合、added の値は false になります。その場合、追加の写真をダウンロードしようとするのを停止します。- 各タスクは、写真のダウンロードを開始する前にキャンセルを確認します。キャンセルされている場合、タスクは nil を返します。
- 最後に、タスクグループは結果を収集する際に nil 値をスキップします。キャンセルを nil を返すことで処理することで、タスクグループはキャンセル時にすでにダウンロードされた写真を部分的な結果として返すことができ、その完了した作業を破棄することを避けられます。
注: タスクの外部からタスクがキャンセルされたかどうかを確認するには、タイププロパティではなくインスタンスプロパティ
Task.isCancelled
を使用します。
キャンセルの即時通知が必要な作業には、Task.withTaskCancellationHandler(operation:onCancel:isolation:)
メソッドを使用します。例えば:
let task = await Task.withTaskCancellationHandler {
// ...
} onCancel: {
print("キャンセルされました!")
}
// ...しばらくして...
task.cancel() // "キャンセルされました!" と表示されます
キャンセルハンドラを使用する場合、タスクのキャンセルは依然として協調的です:タスクは完了するか、キャンセルを確認して早期に停止します。キャンセルハンドラが開始されるとき、タスクはまだ実行中であるため、タスクとそのキャンセルハンドラの間で状態を共有することは避けてください。これは競合状態を引き起こす可能性があります。
非構造化並行処理
前のセクションで説明した構造化された並行処理のアプローチに加えて、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
デタッチされたタスクの管理に関する詳細は、Task
を参照してください。
アクター
タスクを使用してプログラムを分離された並行処理の部分に分割できます。タスクは互いに分離されているため、同時に実行することが安全ですが、タスク間で情報を共有する必要がある場合があります。アクターを使用すると、並行コード間で情報を安全に共有できます。
クラスと同様に、アクターは参照型です。したがって、クラスが参照型であることの比較は、アクターにも適用されます。クラスとは異なり、アクターは一度に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)
// "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:)
メソッドを呼び出します。最初にmeasurements
配列を更新します。 - コードが
max
を更新する前に、他の場所のコードが最大値と温度の配列を読み取ります。 - コードが
max
を変更して更新を完了します。
この場合、他の場所で実行されているコードは、更新の途中でデータが一時的に無効になっているため、誤った情報を読み取ります。Swiftアクターを使用することで、この問題を防ぐことができます。アクターは一度に1つの操作のみをその状態に対して許可し、そのコードはawait
で中断ポイントをマークした場所でのみ中断される可能性があります。update(with:)
には中断ポイントが含まれていないため、更新の途中で他のコードがデータにアクセスすることはできません。
アクター外部のコードが構造体やクラスのプロパティにアクセスするようにこれらのプロパティに直接アクセスしようとすると、コンパイル時エラーが発生します。例えば:
print(logger.max) // エラー
await
を書かずにlogger.max
にアクセスすることは失敗します。アクターのプロパティはそのアクターの分離されたローカル状態の一部であるためです。このプロパティにアクセスするコードはアクターの一部として実行される必要があり、これは非同期操作であり、await
を書く必要があります。Swiftは、アクター上で実行されているコードのみがそのアクターのローカル状態にアクセスできることを保証します。この保証はアクターの分離として知られています。
Swiftの並行処理モデルの次の側面は、共有可変状態についての推論を容易にするために連携します:
- 潜在的な中断ポイントの間のコードは、他の並行コードからの割り込みの可能性なしに順次実行されます。
- アクターのローカル状態と対話するコードは、そのアクター上でのみ実行されます。
- アクターは一度に1つのコードのみを実行します。
これらの保証のおかげで、アクター内のawait
を含まないコードは、他の場所で一時的に無効な状態を観察するリスクなしに更新を行うことができます。例えば、以下のコードは測定された温度を華氏から摂氏に変換します:
extension TemperatureLogger {
func convertFahrenheitToCelsius() {
measurements = measurements.map { measurement in
(measurement - 32) * 5 / 9
}
}
}
上記のコードは測定値の配列を1つずつ変換します。map
操作が進行中の間、一部の温度は華氏であり、他の温度は摂氏です。しかし、このメソッドにはawait
が含まれていないため、このメソッドには潜在的な中断ポイントはありません。このメソッドが変更する状態はアクターに属しており、アクター上で実行されるコード以外がそれを読み取ったり変更したりすることはできません。これにより、単位変換が進行中の間に部分的に変換された温度のリストを他のコードが読み取ることはできません。
一時的に無効な状態を保護するアクター内のコードを書くことに加えて、潜在的な中断ポイントを省略することで、そのコードを同期メソッドに移動できます。上記のconvertFahrenheitToCelsius()
メソッドは同期メソッドであり、潜在的な中断ポイントを含むことは決してありません。この関数はデータモデルを一時的に一貫性のない状態にするコードをカプセル化し、データの一貫性が作業の完了によって回復される前に他のコードが実行されることがないことを認識しやすくします。将来的に、この関数に並行コードを追加し、潜在的な中断ポイントを導入しようとすると、バグを導入する代わりにコンパイル時エラーが発生します。
Sendable 型
タスクとアクターを使用すると、プログラムを安全に並行して実行できる部分に分割できます。タスクやアクターのインスタンス内で、変数やプロパティのような可変状態を含むプログラムの部分は、コンカレンシードメインと呼ばれます。可変状態を含むデータは、重複アクセスを防ぐことができないため、コンカレンシードメイン間で共有することはできません。
あるコンカレンシードメインから別のコンカレンシードメインに共有できる型は、sendable 型として知られています。例えば、アクターメソッドを呼び出す際に引数として渡したり、タスクの結果として返したりすることができます。この章の前の例では、送信可能性については議論していませんでしたが、それらの例では常に安全に共有できる単純な値型を使用しているためです。対照的に、いくつかの型はコンカレンシードメイン間で渡すのに安全ではありません。例えば、可変プロパティを含み、それらのプロパティへのアクセスを直列化しないクラスは、異なるタスク間でそのクラスのインスタンスを渡すと予測不可能で不正確な結果を生じる可能性があります。
型を sendable としてマークするには、Sendable
プロトコルへの準拠を宣言します。このプロトコルにはコード要件はありませんが、Swift が強制するセマンティック要件があります。一般的に、型が sendable であるための方法は3つあります:
- 型が値型であり、その可変状態が他の sendable データで構成されている場合 — 例えば、sendable な格納プロパティを持つ構造体や、sendable な関連値を持つ列挙型。
- 型に可変状態がなく、その不変状態が他の sendable データで構成されている場合 — 例えば、読み取り専用プロパティのみを持つ構造体やクラス。
- 型がその可変状態の安全性を確保するコードを持っている場合 — 例えば、
@MainActor
とマークされたクラスや、特定のスレッドやキューでプロパティへのアクセスを直列化するクラス。
セマンティック要件の詳細なリストについては、Sendable
プロトコルのリファレンスを参照してください。
いくつかの型は常に sendable です。例えば、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
}
型を明示的に sendable でないとマークし、Sendable
プロトコルへの暗黙的な準拠をオーバーライドするには、拡張を使用します:
struct FileDescriptor {
let rawValue: CInt
}
@available(*, unavailable)
extension FileDescriptor: Sendable { }
上記のコードは、POSIX ファイルディスクリプタのラッパーの一部を示しています。ファイルディスクリプタのインターフェースは、開いているファイルと対話するために整数を使用しますが、整数値は sendable であるにもかかわらず、ファイルディスクリプタはコンカレンシードメイン間で送信するのは安全ではありません。
上記のコードでは、FileDescriptor
は暗黙的に sendable である条件を満たしています。しかし、拡張によりその Sendable
への準拠が利用できなくなり、型が sendable であることを防いでいます。