[Swift6] アクター分離境界 (Actor isolation boundary) を理解する
Swift6から本格導入された、Actor isolation boundaryについて解説します。
Swift5でもActorは導入されていましたが、Swift6がリリースされるまでの過程でいくつかの変更が入っており、それらを踏まえたものなので、Swift5のActorの説明とは異なる箇所がある点には注意してください。
また、それなりに調べた上で書いていますが、正確な情報を保証する物ではない為、必要に応じて公式ドキュメントを参照してください。この記事において参照したソースについては、末尾に記載しています。
ActorとActor isolation boundaryの基本的な考え方
ActorとExecutor
同時実行性を制御する概念として、Actorが導入されました。
SwiftにおけるActorは、従来のオブジェクト指向にアクターモデルの概念を取り入れた物で、「同時実行性についての指定をClassに加えた物」と捉えて概ね問題ないでしょう。
Actorは1つのSerialExecutorを(多くの場合は暗黙的に)指定します。
このSerialExecutorはDispatchSerialQueueのようなもので、キューイングされたJobを順番に処理する役割を担います。(便宜上、SerialExecutorのことを指して以降はExecutorと書きます)
例えば、上記の図のようにActor A
とActor B
が同じExecutorを指定した場合、Actor A
とActor B
のメソッドは同時には1つしか実行されないことが保証されます。
逆に、別のExecutor β
を指定したActor C
はActor A
のメソッドと同時に実行される可能性があることを意味します。
Executorによる排他制御で説明しましたが、当然Actor A
のメソッド同士も同時に実行されません。
多くの場合は、Executorを意識することはないのでActorのメソッドは同時に実行されることがないと捉えて問題ありません。
これによって、並列プログラミングによる排他制御を行おうというのが基本的な考え方です。
Actor isolation boundaryとSendable
先ほどの例においてActor A
とActor C
は別のExecutorで実行されるとしました。
では、Actor A
からActor C
の以下のようなメソッドを呼び出す場合はどうなるでしょうか。
この時、RequestとResponseはActor A
とActor C
上の両方で扱われることになり、同じデータ領域に同時に書き込みが行われる可能性があります。
このように複数のActorを跨ぐことを「アクター分離境界を越える (across actor isolation boundaries)」と言います。(Actor boundary, Isolation boundaryなど表記のブレがありますが、以降はアクター境界と書きます)
値コピーされる構造体や、スレッドセーフなクラスを「アクター境界を超えられる」とすることで、Actor間のやり取りが安全であることが分かるようになります。この、「アクター境界」を超えられる値というのが、Sendableです。
Region based isolation
しかし、アクター境界を越える為には、必ずSendable(=スレッドセーフ)でなければならないのでしょうか?
別の形で異なるアクター上で同時に扱われないことが保証されるのであれば、値自体がスレッドセーフでなくても良いはずです。そこで導入されたのが「Region based isolation」です。
このようなコードがあった場合、RequestはActor A
からActor C
に、ResponseはActor C
からActor A
にアクセス権を移譲した形になり、安全なコードであると言えます。
このアクセス権の移譲の必要性を判別できるようにする為に、sendingキーワードが導入されました。sendingキーワードがある引数に渡したインスタンス、もしくはsendingキーワードのある返り値にしたインスタンスは、メンバ変数などに保持しておくことができなくなります。
同時実行制御の為の1単位としてのActor
ここまでで基本的なActorの概念を説明しましたが、実際にはもう少し複雑になります。
前言を撤回するようですが、実際はActorはオブジェクト指向におけるClassと同じレイヤーの存在ではありません。時にはClassの一部がActor上で呼び出されず、また、別のClassがActor上で呼び出されることもあります。
つまり、SwiftにおけるActorは、同時実行制御の為の一つの単位でしかなく、クラス・関数・変数を横断的にグループ化したものです。
ここからは、そのグループ化の仕方と、その意義を解説します。
GlobalActor
iOSアプリ開発において、最も一般的な分離はメインスレッドでの実行を指定することです。
UIViewやUIViewControllerなどの、UIに関する操作は今までもメインスレッドで行う必要がありました。
これらについて都度Actorを定義する必要性はないので、GlobalActorという共通で利用できるActorが定義され、その1つとしてMainActorも定義されました。
このように、既存の変数・関数・クラスに対してGlobalActorを紐づけることが可能となります。
これの便利なところは、同じGlobalActor上であればアクター境界を超えないという点にあります。これによってSendableを意識せずとも、クラス間のやり取りができるようになります。
nonisolated
iOSアプリ開発で広く使われてきたデザインパターンに、Delegateパターンがあります。
例えば、以下のようなDelegateがあった場合に、果たしてこれが実行されるのはどのActorでしょうか?
protocolを実装する場合、protocolで定義されたActorと実装側のActor定義があり、これが一致しない場合に不都合が生じます。
このようなケースに使われるのがnonisolated
(非分離)です。Swift6では、これらが一致しない場合に非分離(= クラスで指定されたActor上では実行されない)とする必要があります。
この場合は、Delegate
が@MainActor
としているので、reload
はActor A
上ではなく、MainActor
上で実行されることになります。
但し、nonisolated
なメソッドからメンバ変数に直接アクセスしようとすると、アクター境界を越えることになるので、コンパイルエラーになります。
isolated
逆に特定のメソッドや変数を指定したActor上に分離することもできます。それがisolated
(分離)です。
これをうまく用いることによって、不必要な非同期処理を削減し、パフォーマンスの向上を図ることができます。
また、アクター境界を越えることができない場合に、この方法でアクター境界を超えない形に実装することもできます。
アクター境界について解説しました。近日中に実践的なSwift6対応についての記事も投稿したいと思います。(多分……)
参考文献
記事が気に入ったらチップを送ることができます!
You can give me a cup of coffee :)
Kyash ID: soranoba
Amazon: Wish List
GitHub Sponsor: github.com/sponsors/soranoba
PayPal.Me: paypal.me/soranoba
(Updated: )