Strict Concurrency Checkingを突破する
この話
まずこれを熟読
Actorの理解が足りてなかったので、公式ドキュメントに戻る
You can explicitly insert a suspension point by calling the Task.yield() method.
ここなんかよくわからない
suspension pointを明示的に指定できるらしいけど、どういうことだろう
func generateSlideshow(forGallery gallery: String) async {
let photos = await listPhotos(inGallery: gallery)
for photo in photos {
// ... render a few seconds of video for this photo ...
await Task.yield()
}
}
レンダリング処理に時間かかるときに指定すると、他のタスクの処理をブロッキングせずに済む?みたいなことが書いてあるが、イマイチ働きが腑に落ちない。
ここで言うyieldは「明け渡す」なんだろうか
Swift Concurrencyはスレッドプログラミングをラップしてる、という考えでいいのかな
Note
If you’ve written concurrent code before, you might be used to working with threads. The concurrency model in Swift is built on top of threads, but you don’t interact with them directly.
// …
Task
- Concurrencyを使ったコードは何らかのTaskを生成している
- StructuredなものとUnstructuredなものがある
Structured Concurrency
- 親タスク・子タスクの関係で構造化されているもの
- キャンセル処理は、親タスクのキャンセルで小タスクも自動でキャンセルされる
- cooperative cancellation modelなので?子タスクのキャンセルでもタスクツリー全体がキャンセル扱いになる?
- ここはちょっとちゃんとわかってない
- こっちでコード書くことは経験上ないが、
async-let
の並列処理が暗黙的にstructured taskを使っているので注意が必要 - 自分でタスクツリーを実装するとしたら、
TaskGroup
を使う
Unstructured Concurrency
-
Task { … }
で書くやつ - 使い勝手が良い
Actor
- 参照型
- 同時に1タスクの実行しか許可しないインスタンスを生成する
- (アクター隔離)
- プロパティ・メソッドへのアクセスはawaitキーワードが必要
- 名前はアクターモデルのActorかららしい
- 「並行性を持つオブジェクト」みたいな概念らしい
Concurrency: 並行性
並列(parallel)、と似てるが、厳密には違う概念っぽい。
同時&並列というのがポイント。
同時に同じ資源に対してアクセスするプロセスがあっても破綻しない設計が要求されるとのこと。
まだ疑問が残ってるポイント
-
Task { … }
とasync let
で切ったところって、actorどうなってる?- アクターとは別概念なのかな?
- どのドメインで動いてるのか
-
Task { … }
とTask.detached(priority:operation:)
の違い
ここまでがSendableチェックについて考えるための前提知識。なが
なぜSendableチェックが問題になったか
- ↓この方針で、Swift Concurrencyに書き換えていた
- https://zenn.dev/st43/articles/3596e8b3483894
- Presenterに@MainActorを付与して、ViewControllerから
Task { … }
で呼び出しという方針 - Sendableチェックのことを考えずに導入したため、コンパイラでワーニング出まくる状態になった
- Swift 6になるとコンパイルできなくなるからなんとかしたい
Sendableとは
- Concurrency Domainを超えてもデータ競合が発生しないことが保障されていることを示すプロトコル
- Concurrency Domain: タスクのインスタンスやアクターのインスタンスの保持している状態
- 具体的には変数やプロパティ
- 下記がSendable(公式ドキュメント)
- Value types
- Reference types with no mutable storage
- Reference types that internally manage access to their state
- Functions and closures (by marking them with @Sendable)
- 上三つは暗黙的にSendable準拠となる
- 要はmutableな値を持っていなければOK
- どうしてもmutableにやってるところは……?
Sendableチェック
- セマンティックにやっているので、コンパイル前に解析が可能
- public structだとSendable扱いじゃない
- なぜだ、と思っていたが、下記の説明がしっくりきた
これは public な型はモジュールのインターフェースであり、インターフェースが特定のプロトコルに暗黙的に準拠するのは互換性の観点から好ましくないからだと思います。
- @MainActorも暗黙的にSendableが付与される。これがまたややこしい
- 外部ライブラリでConcurrency対応できていないものは
@preconcurrency import
でワーニングを消すことができる
- どうしてもミュータブルなプロパティ持ってるクラスは、アクター超えてやり取りをすべきでない
-
NSAttributeString
とか。これはそもそもあんまりクラス外に受け渡さないか
-
だいたいキャッチアップできた気がするので、あとは疑問解消
Task { … }
と async let
で切ったところって、actorどうなってる?
- コンテキストがクロージャーの処理に引き継がれるかどうかは、クロージャー自身がSendableかどうかに依存
- Sendableでないとき引き継ぐ
- Arrayの
map
なんかはこの方が都合がいい
- Arrayの
- Sendableなら引き継がれない
- コピーオンライトの方が何かと都合がいい
- Sendableでないとき引き継ぐ
-
@escaping
属性はこの挙動に影響を与えない -
Task { … }
のクロージャーはSendableが付与されている - じゃあコンテキスト引き継がないルールだが、
Task { … }
は引き継ぐ-
@_inheritActorContext
という属性で実現している
-
-
async let
は内部的には子タスクを切るので、別コンテキストとなる
※Case 20の注に記載あり
Task { … }
と Task.detached(priority:operation:)
の違い
ここに書いてあった
To create an unstructured task that runs on the current actor, call the Task.init(priority:operation:) initializer. To create an unstructured task that’s not part of the current actor, known more specifically as a detached task, call the Task.detached(priority:operation:) class method.
(拙訳)現在のアクターで動作するunstructured taskを生成するためには、イニシャライザのTask.init(priority:operation:) を使います。現在のアクターではなく、切り離されたタスクとして生成するならば、クラスメソッドのTask.detached(priority:operation:)を使います。
あとちょっとTaskとActorの関係がわからなくなったので、整理したい
アクターモデルの文脈のアクターが、プロセス(OSのプロセスとは別)みたいなものなので、タスクとの差異がわかりづらい
から
Task
A task is a unit of work that can be run asynchronously as part of your program
これはまあなんかある意味言葉通りで良さそう
Actor
You can use tasks to break up your program into isolated, concurrent pieces. Tasks are isolated from each other, which is what makes it safe for them to run at the same time, but sometimes you need to share some information between tasks. Actors let you safely share information between concurrent code.
タスク間で安全にデータを受け渡すときに利用するのがActor、と書かれている
アクターモデルの文脈のアクターが、プロセス(OSのプロセスとは別)みたいなものなので、タスクとの差異がわかりづらい
この理解が間違ってるな
ここのErlang プロセスについての記述から、勘違いが生まれてしまった
オブジェクト指向プログラミングにおけるオブジェクトに相当する概念がアクター
この記述がわかりやすかったかも
アクターは内部にカプセル化された状態を持ちます。
Sendableチェックに対応する
- 可能であればModelクラスをイミュータブルなつくりにする
- ミュータブルなものはactor化する
どうしても対応できないもの
- しかし大量の箇所で利用されているModelをactor化した場合、それ全部await付与するの?という問題になる
- データ競合の可能性がないものであれば、
@unchecked Sendable
を付与することで、逃げられる- 当然、並行処理の安全性は下がるので乱用すべきではない
- 外部ライブラリの場合は
@preconcurrency import
-
deinit
がアクター隔離対象じゃないらしく、その中でselfを利用してるコードは軒並みCall to main actor-isolated instance method 'xxx' in a synchronous nonisolated context
でコンパイルエラーになる - Swift Evolutionでも議論してるの見つけたけど、いい対応なさそう
- https://forums.swift.org/t/deinit-and-mainactor/50132
Sendableチェックというタイトルは不適当だな。
Strict Concurrency Checking対応の方がいいのかな
デフォルト引数にMainActorのクラス指定してると、synchronous nonisolated context扱いになるのも地味に困る