Closed37

Strict Concurrency Checkingを突破する

蔀

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かららしい
    • 「並行性を持つオブジェクト」みたいな概念らしい
蔀
  • アクター隔離が言語設計として保障されているので、アクターの内部ではawaitキーワードをつける必要はない
  • クラスはデータ競合の危険がある場合、プログラマが何らかのアクションをする必要があった
    • ロックをかけるか
    • 同一スレッドを使ったキューイングを利用するか
    • actorを使えば、データ競合の排他が保障されているので安心
蔀

Concurrency: 並行性

並列(parallel)、と似てるが、厳密には違う概念っぽい。
同時&並列というのがポイント。
同時に同じ資源に対してアクセスするプロセスがあっても破綻しない設計が要求されるとのこと。

https://ja.wikipedia.org/wiki/並行性

蔀

まだ疑問が残ってるポイント

  • 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チェック

  • セマンティックにやっているので、コンパイル前に解析が可能
蔀
  • @MainActorも暗黙的にSendableが付与される。これがまたややこしい
蔀
  • どうしてもミュータブルなプロパティ持ってるクラスは、アクター超えてやり取りをすべきでない
    • NSAttributeString とか。これはそもそもあんまりクラス外に受け渡さないか
蔀

だいたいキャッチアップできた気がするので、あとは疑問解消

蔀

Task { … } async let で切ったところって、actorどうなってる?

蔀
  • コンテキストがクロージャーの処理に引き継がれるかどうかは、クロージャー自身がSendableかどうかに依存
    • Sendableでないとき引き継ぐ
      • Arrayのmap なんかはこの方が都合がいい
    • Sendableなら引き継がれない
      • コピーオンライトの方が何かと都合がいい
  • @escaping 属性はこの挙動に影響を与えない
  • Task { … }のクロージャーはSendableが付与されている
  • じゃあコンテキスト引き継がないルールだが、Task { … }は引き継ぐ
    • @_inheritActorContext という属性で実現している
  • async let は内部的には子タスクを切るので、別コンテキストとなる

※Case 20の注に記載あり

https://zenn.dev/koher/articles/swift-concurrency-cheatsheet#💼-case-20-(mainactor)%3A-共有された状態の変更(メインスレッド上での処理)

蔀

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のプロセスとは別)みたいなものなので、タスクとの差異がわかりづらい

蔀

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/

から

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扱いになるのも地味に困る

このスクラップは2024/01/07にクローズされました