16
3

[Swift] isolatedなasync functionからnon-isolatedなasync functionを呼び出した時は必ずActorから出る

Last updated at Posted at 2023-11-30

TL;DR

Swift5.7以降では

@MainActor
func isolatedFunc() async {
    await nonIsolatedFunc()
}

func nonIsolatedFunc() async {
    // ここはMainActorではない
}

Swift5.6以前では

@MainActor
func isolatedFunc() async {
    await nonIsolatedFunc()
}

func nonIsolatedFunc() async {
    // ここはMainActor
    ...
    
    try? await Task.sleep(nanoseconds: 100)
    
    // ここはMainActorではない
    ...
}

参考

事前知識

isolatedなasync function

あるactor上での実行が保証されているasync function

  • globalActorで明示されたasync function
@MainActor
func isolatedFunc() async {
    // 必ずここはMainActor
}
  • globalActorによって明示された型またはactor含まれたasync function
@MainActor
class Hoge {
    func isolatedFunc() async {
        // 必ずここはMainActor
    }
}

@MainActor
struct Hoge {
    func isolatedFunc() async {
        // 必ずここはMainActor
    }
}

actor HogeActor {
    func isolatedFunc() async {
        // 必ずここはHogeActor
    }
}

non-isolatedなasync function

どのactor上での実行も保証されていないasync function

  • 何にも明示されていない型に含まれたasync function
class Hoge {
    func nonIsolatedFunc() async {
    }
}

struct Hoge {
    func nonIsolatedFunc() async {
    }
}
  • 明示された型やactorに含まれているが、nonisolatedキーワードがついているasync function
@MainActor
class Hoge {
    nonisolated func nonIsolatedFunc() async {
    }
}

@MainActor
struct Hoge {
    nonisolated func nonIsolatedFunc() async {
    }
}

actor HogeActor {
    nonisolated func nonIsolatedFunc() async {
    }
}

本題

「isolatedなasync function」から「non-isolatedなasync function」を呼び出した時、「non-isolatedなasync function」は、「isolatedなasync function」のActorを引き継ぐのか?

@MainActor
func isolatedFunc() async {
    await nonIsolatedFunc()
}

func nonIsolatedFunc() async {
    // ここはMainActorか?
}

実験

@MainActor
func isolatedFunc() async {
    await nonIsolatedFunc()
}

func nonIsolatedFunc() async {
    MainActor.shared.assertIsolated()
}

await isolatedFunc()

Swift 5.9

クラッシュするのでnonIsolatedFuncはMainActorではないことがわかります。

なぜ?

SE-0338に記載があります。

actor-isolated async functions always formally run on the actor's executor
non-actor-isolated async functions never formally run on any actor's executor

never formally run on any actor's executor なので 絶対にisolatedなasync functionのActor executorを引き継がない ということになります。

これはSwift5.7からの仕様です。
SE-0338のStatusが Implemented (Swift 5.7) となっているからです。

Swfit5.6以前の仕様についても言及があります。

In the current implementation, calls and returns from actor-isolated functions will continue running on that actor's executor.(中略)
they will remain there until either the task suspends or it needs to run on a different actor.

current = 5.6以前ではwill continue running on that actor's executorなのでisolatedなasync functionのActor executorを引き継ぐ ということになります。

また、 suspendするかほかのActorで実行されるまで引き継ぐ ので以下のような挙動になります。

@MainActor
func isolatedFunc() async {
    await nonIsolatedFunc()
}

func nonIsolatedFunc() async {
    // ここはMainActor
    ...
    
    try? await Task.sleep(nanoseconds: 100)
    
    // ここはMainActorではない
    ...
}

@_unsafeInheritExecutor

上までの話は例外があります。例えばwithCheckedContinuationです。

@MainActor
func isolatedFunc() async {
    await withCheckedContinuation { continuation in
        MainActor.shared.assertIsolated()
        print("OK")
        continuation.resume(returning: ())
    }
}

await isolatedFunc()

これは@_unsafeInheritExecutorというattributeによるものです。これによって呼び出し元(今回のケースではisolatedなasync functionのActor executor)を引き継ぐ挙動をとります。

実際にwithCheckedContinuationにはこのattributeがついています。

SE-0338実装以前に@_unsafeInheritExecutorは存在しませんでした。SE-0338以後にwithCheckedContinuationでexecutorを引き継ぐ挙動を再現するために@_unsafeInheritExecutorが実装されたという経緯です。

@_unsafeInheritExecutorの実装PR

どうすればいい?

Swift5.6以前の挙動を確認してActorを引き継ぐ前提のコードを書いている場合は修正する必要がありそうです。

struct HogeView: View {

    ...
    
    var body: some View {
        VStack {
            Button {
                Task {
                    await hogeViewModel.onButtonTapped()
                }
            } label: {
                Text("load")
            }
        }
        .task {
            await hogeModel.needCallFromMainThread()
        }
    }
}

struct HogeModel {
    // @MainActor をつけるべき!!
    func needCallFromMainThread() async {
        await ...
    }
}

// @MainActorをつけるべき!!
final class HogeViewModel: ObservableObject {
    // PublishedはMainActorからしか更新してはいけない
    @Published private(set) var isLoading = false

    func onButtonTapped() async {
        isLoading = true
        ...
    }
}
16
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
3