ゆる〜く学ぶSwiftのConcurrency ~後半~
この記事はSwift/Kotlin愛好会 Advent Calendar 2021 5日目の記事です!
この記事はSwift.orgのConcurrencyをゆる〜く日本語で読み下したものです。これからConcurrency学ぶよって方はこの記事でざっくり内容を掴んでからぜひ原文も読んでみてください〜
前半はこちら
以下本文
TasksとTask Groups
Task(タスク)とは非同期に実行できる処理の単位だよ。すべての非同期コードはTaskの一部として実行されるよ。async-let
構文は子タスクを作るんだ。
Task Group(タスクグループ)を作って子タスクを追加することもできるよ。これにより優先度とキャンセルをより細かく制御できるようになる。
Taskは階層として配置されるよ。それぞれのTask Groupの中のTaskは同じ親Taskを持っていて、それぞれが子Taskも持てるよ。
TaskとTask Groupに明示的な関係性があるので、このアプローチは Structured Concurrency(構造化された並行性)と呼ばれるんだ。
プログラムの正当性はある程度開発者が担保する必要があるけど、Task同士の明示的な親子関係のおかげで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を見てね。
非構造的な並行性
前述した構造的な並行性(Structured Concurrency)へのアプローチに加えてSwiftは非構造的な並行性(Unstructured Concurrency)もサポートしてるよ。非構造的なTaskは親Taskを持たないんだ。
実装のニーズに合わせて完全に柔軟にTaskを扱えるけどプログラムの正当性はすべて開発者が担保する必要があるよ。
現在のActor(後述)で実行される非構造的Taskを作るにはTask.init(priority:operation:)
イニシャライザーを呼んでね。
現在のActorの一部ではない非構造的Task(具体的にはデタッチされたTask(detached task)と呼ばれる)を作るには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の扱い方の詳細はTaskを見てね。
Taskのキャンセル
Swiftの並行性には協調的なキャンセルモデルを採用しているよ。
それぞれのTaskは実行中の適切なポイントでキャンセルされたかどうかをチェックして、適切な方法でキャンセルの応答を返すんだ。通常はだいたい次のうちのどれかになるよ。
-
CancellationError
のようなエラーを投げる - nilか空のコレクションを返す
- 部分的に完了した結果を返す
キャンセルをチェックするためには、キャンセルされたらCancellationError
を投げるTask.checkCancellation()
を呼ぶか、Task.isCancelled
の値をチェックして独自にキャンセル処理をするかの方法があるよ。
例えばギャラリーから写真をダウンロードするTaskは、部分的にダウンロードしたデータを削除したり、ネットワークのコネクションを閉じたりしたくなるときがきっとあるよね。
マニュアルでキャンセルを伝播させるにはTask.cancel()
を呼ぶよ。
Actors
Classと同様、Actor(アクター)も参照型だよ。Classes Are Reference Types で比較されている値型と参照型の比較はClassと同様Actorにも当てはまるよ。
Actorは一度に1つのタスクのみが可変状態にアクセスできるので、同一のActorのインスタンスに複数のTaskから安全にアレコレできるよ。例えば、気温を記録するActorは次のとおりだよ。
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
キーワードでActorを定義できるよ。TemperatureLogger
Actorの例では、Actorの外側からアクセスできるプロパティがあるけど、Actor内のコードのみがmax
プロパティの最大値を更新できるように制限されているよ。
ActorのインスタンスはStructやClassと同じ構文のイニシャライザーで作れるよ。あとActorのプロパティやメソッドにアクセスするときは、一時停止の可能性があるポイントをawait
で指定するよ。例えばこんな感じ
let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max)
// Prints "25"
この例ではlogger.max
は一時停止の可能性があるポイントなんだ。Actorは一度に1つのTaskしか可変な状態にアクセスできないので、他のTaskがloggerとアレコレしているしている間はプロパティにアクセスできるようになるまで一時停止して待つよ。
対照的に、Acotorの中のコードからActorのプロパティにアクセスするときにawait
は不要だよ。例えばTemperatureLogger
を新しい温度で更新するメソッドはこんな感じ。
extension TemperatureLogger {
func update(with measurement: Int) {
measurements.append(measurement)
if measurement > max {
max = measurement
}
}
}
update(with:)
メソッドはActorで実行されているのでmax
プロパティにアクセスするときもawait
は不要だよ。
このメソッドはActorがなぜ同時に1つだけのTaskからしか可変な状態にアクセスを許可しないかの理由も示しているよ。
例えばActorの状態を更新すると、一時的に不変性が壊れるよね。TemperatureLogger
Actorは温度と最大温度のリストをトラックし、新しい測定値が記録されると最大温度を更新する。更新の途中で新しい測定値を追加した後、かつmax
を更新する前のTemperatureLogger
は一時的に一貫性のない状態になっているんだよ。複数のTaskが同じインスタンスと同時にアレコレするのを防ぐことで、次のような問題を防ぐことができるんだ。
-
update(with:)
メソッドを呼び出して、measurements Arrayをまず更新する -
max
を更新する前に最大値と温度のArrayを読み取る -
max
を変更して更新を完了する
このケースでは他の場所から実行されているコードはデータが一時的に無効な状態であるupdate(with:)
が呼ばれている間にActorへのアクセスが差し込まれてしまうので、誤った情報を読み取ってしまうね。
SwiftのActorを使うと、一度に1つの操作しかできず、await
があるところでのみコードを中断できるからこの問題を防ぐことができるよ。update(with:)
は一時停止するポイントは含まれてないので更新の途中に他のコードはデータにアクセスできないよ。
もしActorの外からこれらのプロパティにアクセスしようとしたらコンパイルエラーになるよ。
print(logger.max) // Error
Actorのプロパティは分離されたローカルの状態の一部なのでawait
なしでlogger.max
にアクセスできないよ。SwiftはActor内のコードのみがActorのローカル状態にアクセスすることを保証するんだ。この保証のことを、actor isolation
(Actorによる隔離)と言うよ。
本文以上!
Discussion