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
...
}
}