🔍

Thread.isMainThread の MainActor 版っぽいものがほしい(自分が今いる actor を調べる)

2023/07/26に公開

まとめ

func exampleForMainActor() {
    // DEBUG ビルドのとき、
    // ここが Main Actor のコンテキストで実行されていなければ
    // ここでプログラムの実行が停止する
    MainActor.assertIsolated()
    
    // DEBUG ビルド・RELEASE ビルドの両方で、
    // ここが Main Actor のコンテキストで実行されていなければ
    // ここでプログラムの実行が停止する
    MainActor.preconditionIsolated()
}

// `assertIsolated(_:file:line:)`・`preconditionIsolated(_:file:line:)` は
// `GlobalActor` の Type Methods なので、自作の Global Actor でも使える
@globalActor
actor BackgroundActor: GlobalActor { /* ... */ }
func exampleForBackgroundActor() {
    BackgroundActor.assertIsolated()
    BackgroundActor.preconditionIsolated()
}

// `Actor` の Instance Methods にも
// `assertIsolated(_:file:line:)`・`preconditionIsolated(_:file:line:)` がある
actor MyActor { /* ... */ }
let myActor = MyActor()
func exampleForMyActor() {
    myActor.assertIsolated()
    myActor.preconditionIsolated()
}

自分は今どの Global Actor のコンテキスト内にいるのか

Swift Concurrency において、非同期なメソッド等を呼び出す際に Task を作ったり、SwiftUI であれば task(priority:_:) などを使ったりします。

既存のコードたちを Swift Concurrency 向けに移行していく際、「今自分がいる Task はどの actor のコンテキストを引き継いでいるのか?」を気にしたくなるときがあります。

Thread.isMainThread を使うとコンパイル時に warning が出る

メインスレッドが求められていた状況を考えます。これまで、自分がいたスレッドがメインスレッドであるかどうかを知る手段の1つとして、Thread.isMainThread を使うことがありました。

struct ContentView: View {
    var body: some View {
        Text("Hello, happy world!")
            .task {
                let isMainThread = Thread.isMainThread
                assert(isMainThread) // DEBUG ビルドのとき、ここがメインスレッドで実行されていなければここでプログラムの実行が停止する
                precondition(isMainThread) // DEBUG ビルド・RELEASE ビルドの両方で、ここがメインスレッドで実行されていなければここでプログラムの実行が停止する
            }
    }
}

上記のコード例の場合、SwiftUI の Viewbody には @MainActor が付与されているため、Thread.isMainThreadtrue となります。

しかし、コンパイル時にはこのような warning が表示されていました。

Class property 'isMainThread' is unavailable from asynchronous contexts; Work intended for the main actor should be marked with @MainActor; this is an error in Swift 6

これをほぼ代替する仕組みが Swift 5.9(SE-0392)にて用意されました。

Apple プラットフォームでは iOS 17.0 以降、iPadOS 17.0 以降、macOS 14.0 以降、tvOS 17.0 以降、watchOS 10.0 以降、visionOS 1.0 以降で利用できる方法です。

DEBUG 向けの assertIsolated(_:file:line:)

まずは assert(isMainThread) の方を見ます。ここで使われている assert(_:_:file:line:) は、Swift コンパイラの最適化レベルが -Onone(Xcode の DEBUG ビルドのデフォルト設定)のとき、condition の値が false であればそこでプログラムの実行が停止します。

これと同じ用途で用いることができるのが assertIsolated(_:file:line:) です。

struct ContentView: View {
    var body: some View {
        Text("Hello, happy world!")
            .task {
                MainActor.assertIsolated() // DEBUG ビルドのとき、ここが Main Actor のコンテキストで実行されていなければここでプログラムの実行が停止する
            }
    }
}

DEBUG・RELEASE 向けの preconditionIsolated(_:file:line:)

次に precondition(isMainThread) の方を見ます。ここで使われている precondition(_:_:file:line:) は、Swift コンパイラの最適化レベルが -Onone(Xcode の DEBUG ビルドのデフォルト設定)や -O(Xcode の RELEASE ビルドのデフォルト設定)のとき、condition の値が false であればそこでプログラムの実行が停止します。

これと同じ用途で用いることができるのが preconditionIsolated(_:file:line:) です。

struct ContentView: View {
    let queue = DispatchQueue.main
    
    var body: some View {
        Text("Hello, happy world!")
            .task {
                MainActor.preconditionIsolated() // DEBUG ビルド・RELEASE ビルドの両方で、ここが Main Actor のコンテキストで実行されていなければここでプログラムの実行が停止する
            }
    }
}

@MainActor 以外の Global Actor でも利用できる

assertIsolated(_:file:line:)preconditionIsolated(_:file:line:)MainActor が適合している GlobalActor に定義されています。

そのため、これまで Thread などでは困難だった「今その Global Actor のコンテキスト内にいるか?」を調べることができます。

@globalActor
actor BackgroundActor: GlobalActor {
    static let shared = BackgroundActor()
}

struct ContentView: View {
    var body: some View {
        Text("Hello, happy world!")
            .task {
                _ = await Task { @BackgroundActor in
                    BackgroundActor.assertIsolated()       // ✅
                    BackgroundActor.preconditionIsolated() // ✅
                }.value
		
		_ = await Task {
                    BackgroundActor.assertIsolated()       // 💥
                    BackgroundActor.preconditionIsolated() // 💥
                }.value
            }
    }
}

Global Actor ではない Actor でも使える

これまでは Global Actor の assertIsolated(_:file:line:)preconditionIsolated(_:file:line:) を紹介しましたが、Actor にも assertIsolated(_:file:line:)preconditionIsolated(_:file:line:) が用意されています。

actor MyActor {}

struct ContentView: View {
    let myActor = MyActor()
    
    var body: some View {
        Text("Hello, happy world!")
            .task {
                myActor.assertIsolated()       // 💥
                myActor.preconditionIsolated() // 💥
            }
    }
}

注意点

GlobalActorassertIsolated(_:file:line:)preconditionIsolated(_:file:line:)ActorassertIsolated(_:file:line:)preconditionIsolated(_:file:line:) はいずれも Swift Concurrency への移行作業中に、コードが意図した actor のエグゼキュータで動作しているかどうかを保証する方法として提供されています。

本来はこれらのアサーションを使わず、actor のメソッドにしたり Global Actor を付与したりするのがベストです。

参考

Discussion