The Pragmatic Ball boy

iOSを中心にやってる万年球拾いの老害エンジニアメモ

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())
        }
    }
}

参考: How @MainActor works

MainActorの場合はこのようにメインスレッドで実行されますが、普通のGlobalActorはデフォルトのSerialExecutorによって順次実行されます。このSerialExecutorはSerial Dispatch Queueのときと同様で順次実行はされますが同一のスレッドで実行されるわけではないということは頭に入れておいたほうが良いかなと思います。(同一スレッドでの実行を前提としたコードを書かない) どうしてもMainActorみたいな同一スレッドを保証したGlobalActorを作りたい場合はNSThreadとかをつかって固定スレッドになるようにSerialExecutorを実装すればできると思いますが、固定スレッドをつかってしまうとSwiftConcurrencyのCPUに対して1スレッドが割り当ててスレッドの切り替えを少なくするという特性を阻害することになると思うのでなるべくやめといたほうが良いかと思います。