【翻訳】Swift Concurrency Deep Dive [5] — Actor
この投稿は Swift の並行処理、つまり async/await をより深く理解するために書かれています。
Apple Developer's Document、Swift-evolution リポジトリ、 Swift Language Guide のような信頼できる情報源から可能な限り情報を集めましたが、間違った情報が含まれているかもしれません。その場合は、コメントで教えてください。
私の以前のシリーズを読むことを強くお勧めします
Swift Concurrency Deep Dive [1] — GCD vs async/await (https://towardsdev.com/swift-concurrency-deep-dive-1-gcd-vs-async-await-280ac5df7c76)
Swift Concurrency Deep Dive [2] — Continuation (https://enebin.medium.com/swift-concurrency-deep-dive-2-continuation-c2e385b11a10)
Swift Concurrency Deep Dive [3] — Structured concurrency (https://enebin.medium.com/swift-concurrency-deep-dive-3-structured-concurrency-bcfa7c68b0ba)
Swift Concurrency Deep Dive [4] — Task (https://enebin.medium.com/swift-concurrency-deep-dive-4-task-7743f0eb1ca8)
Actor model
Swift の actor に入る前に、まず actor model とは何かについて知っておく必要があります。
actor model は、並行プログラミングにおける共有リソースの管理を支援するために導入されました。actor には2つの特徴があります。
-
actor にアクセスするタスクは、シリアル・キューのように1つずつ実行されなければなりません。
-
変更可能な状態は、他の actor 間で共有してはいけません。
これらの特徴により、 actor は並行プログラミングの根深い問題であるデッドロックや競合状態から解放されます。
Actor in Swift
from Language Guide
Task を使って、プログラムを分離された同時実行の断片に分割することができます。 Task は互いに分離されているため、同時に実行しても安全ですが、 Task 間で情報を共有する必要がある場合もあります。 actor を使えば、並行コード間で安全に情報を共有できます。
Swift の並行処理も actor model を採用しています。クラス(提案ドキュメント)を作成するように、 actor キーワードを使用して独自の actor を作成することができます。
actor は、 WWDC のセッションを参照し、"Sea of concurrency"(同時実行の海)と表現されるように、プロパティを他の Task から隔離しておきたい場合に使用できます。
Task との関係
Task を作成するとき、現在の actor のコンテキストを継承することを覚えていますか?ここで、 actor と Task の関係は?
Task は、 Swift の並行処理におけるジョブの単位として、割り当てられた命令を含み、実行します。一方、 actor は Task とは全く異なります。
actor は Swift の一種です。それは、同時に複数の場所からアクセスされないように保護された変数を含んでいます。また、それらの変数で実行できる関数を持つことができます。
ここで、 Task は actor の単位ではなく、 Swift の並行処理のための単なる仕事の単位であることを思い出させる必要があります。つまり、 Task は基本的に actor とは何の関係もありません。 actor の内部で実行される Task もあれば、そうでない Task もあります。
混乱するかもしれません。ここで一つ知っておくべきことは、全ての Task は必ずしも actor 上で実行されているわけではないということです。 Task は、 Swift の同時実行のコンテキストのどこにでも存在できます。 Task に関して、 actor は Task 間のデータ共有のための補助型とみなすことができます。
非構造化同時実行との関係
ドキュメントによると、 Task.init によって作成された非構造化同時実行は、その actor を継承します。一方、Task.detached によって切り離されて作成された場合は、現在の actor を継承しません。
Task が所属する actor がない場合、デタッチされた Task と通常の Task の違いは何でしょう?
Swift の開発者の答えによると、どちらも actor を継承せず、協調スレッドプールで実行されるので、スレッドの観点からは同じだそうです。
しかし、一般的には同じではありません。"Task.init による非構造化 Task の作成は、アクターコンテキストを継承するだけでなく、タスクローカル値も継承します。回答によると、「Task.detached は継承しない」そうです。
協調スレッドプール
from the Swift forum
Task (どの actor にもない)は、構造化されていないため、他のSwift Taskと同じように、協調スレッドプールで実行されます。それはどの actor 上でも実行されませんが、@Douglas_Gregor が "同時実行の海" と呼ぶものです。
Task がどの actor にも属さない場合、 Task はどこで実行されるのでしょうか? actor から独立して実行される Task は、「協調スレッドプール」と呼ばれる共有スレッドを使用します。
協調スレッドプールは、 Swift の並行処理で Task を実行するための、あらかじめ作られたスレッド候補群のようなものです。また、協調スレッドプールは複数のスレッドを持っているので、プール内で Task を並列に実行することが可能です。
Task で作成された非構造化並行処理と一緒に、 async let または TaskGroup で作成された子 Task は、どの actor にも属さない場合、協調スレッドプールで実行されなければなりません。
アクターとスレッドプールのパフォーマンス最適化
actor は一度に1つの Task しか実行しないため、1つの actor で全ての Task を実行するとボトルネックになる可能性があります。そのため、割り当てられたジョブを他のスレッドや actor に分散するのが適切です。
そのためには、 nonisolated キーワードを使用します。これは、 actor の Task が必要なときだけ actor にアクセスするようにします。こうすることで、各 Task の残りの部分は、協調スレッドプール上で actor に関連しないジョブを行うことができ、ボトルネックの危険性を解決することができます。
MainActor
MainActor という特別な actor があります。 MainActor は、 DispatchQueue のメイン・キューが行うことと全く同じことを行います。 MainActor はシステム内でメインスレッドを使用する唯一の actor です。
今まで使ってきたメインキューと同じように、UIジョブのような最優先で処理する必要のある Task はここで実行されるはずです。 UIKit の最新バージョンを見ると、 UIViewController などのUI関連のクラスはすでにMainActor に変更されていることがわかります。
MainActor ラッパーの @MainActor を使って作成することができます。
【翻訳元の記事】
Swift Concurrency Deep Dive [5] — Actor
Discussion