UIViewControllerからMainActorを呼ぶときにawaitしなくてよい理由
SwiftConcurrencyでは普通actorのメソッドを呼んだりするときはawaitしなければならないですが、UIViewControllerからMainActorのメソッドを呼ぶときはawaitせずに普通に呼べてしまいます。
これはなぜかというとUIViewControllerもMainActorだからです。
UIViewControllerの定義を見ると以下のようになっています。
@MainActor open class UIViewController : UIResponder, NSCoding, UIAppearanceContainer, UITraitEnvironment, UIContentContainer, UIFocusEnvironment {
MainActorとは
MainActorはGlobalActorの一種です。 GlobalActorは普通のActorだとそのActor内でしかisolatedな状態にできませんが、GlobalActorを使うことでその範囲を外部に広げることができるようになります。
このように別のactor間だとそれぞれでactor isolatedされてるのでお互いを呼ぶときはawaitしなければなりません。
actor A { func execA() { Task { let b = B() await b.execB() } } } actor B { func execB() { } }
ここでGlobalActorをA,Bに適用するとこれらを同じActor isolatedな状態にできるため、awaitを使わずに呼び出すことが可能となります。
@MyGlobalActor struct A { func execA() { let b = B() b.execB() } } @MyGlobalActor struct B { func execB() { } } @globalActor final public actor MyActor : GlobalActor { public static let shared: MyActor }
さらにMainActorはメインスレッドのみで実行されるという特徴を持ちます。UI関連の処理を行う場合はMainActorを使うと便利です。
MainActorの実装について
ところで、MainActorはこのようにGlobalActorとして定義してやるだけでつくれるでしょうか?
@globalActor final public actor MyActor : GlobalActor { public static let shared: MyActor }
これだけではMainActorの重要な要素である メインスレッドで実行する
が適用されません。
MainActorではメインスレッドでの実行保証をどうやって実現しているのでしょうか?
実行スレッドの固定は CustomExecutor によって実現されています。
GlobalActorはactor上でメソッドなどが実行されるときにどのように実行されるかをカスタムできるようになっています。
このようにMainActorではenqueue(job:)でジョブがキューイングされたら順番にメインスレッドで走るようなCustomExecutorを実現しているのでメインスレッドで動く様になっていると思われます。
@globalActor public actor MyMainActor: GlobalActor { public static var shared = MyMainActor() nonisolated let executor: SerialExecutor nonisolated public let unownedExecutor: UnownedSerialExecutor init() { let executor = MainExecutor() self.executor = executor unownedExecutor = executor.asUnownedSerialExecutor() } } final class MainExecutor: SerialExecutor { func asUnownedSerialExecutor() -> UnownedSerialExecutor { UnownedSerialExecutor(ordinary: self) } func enqueue(_ job: UnownedJob) { DispatchQueue.main.async { job._runSynchronously(on: self.asUnownedSerialExecutor()) } } }
MainActorの場合はこのようにメインスレッドで実行されますが、普通のGlobalActorはデフォルトのSerialExecutorによって順次実行されます。このSerialExecutorはSerial Dispatch Queueのときと同様で順次実行はされますが同一のスレッドで実行されるわけではないということは頭に入れておいたほうが良いかなと思います。(同一スレッドでの実行を前提としたコードを書かない) どうしてもMainActorみたいな同一スレッドを保証したGlobalActorを作りたい場合はNSThreadとかをつかって固定スレッドになるようにSerialExecutorを実装すればできると思いますが、固定スレッドをつかってしまうとSwiftConcurrencyのCPUに対して1スレッドが割り当ててスレッドの切り替えを少なくするという特性を阻害することになると思うのでなるべくやめといたほうが良いかと思います。