🧵
await MainActor.run { ... }とawait Task { (at)MainActor in ... }の違い
はじめに
Swift ConcurrencyでのMainActor.run
というクラスメソッド呼び出しと、よくあるTask { @MainActor in ... }
に違いはあるのかどうかというはなし。結果違いがある。
もし何かご意見などあればコメントを下さい
最初に結論
- 違いはあるが公式リファレンス見ても分かりづらい
-
MainActor.run
の特徴- クロージャに入れられるのは
Sendable
に準拠してるかチェックされてそう - クロージャに入れられるのは同期関数のみ(asyncな関数をawaitできない)
- クロージャに入れられるのは
具体例
違いが明確にならない例
まずMainActor.run
のわかりやすい例として次のようなのがある
func couldBeAnywhere() async {
await MainActor.run {
print("This is on the main actor.")
}
}
そしてTask { @MainActor in ... }
の例が次。
func couldBeAnywhere() async {
let task = Task { @MainActor in
print("This is on the main actor.")
}
await task.value
}
// もしくは
func couldBeAnywhere2() async {
await Task { @MainActor in
print("This is on the main actor.")
}.value
}
この例での比較はシンプルすぎて違いはない。
違いが明確になる例
非同期関数をそれぞれから呼び出す
Task { @MainActor in ... }
から非同期関数fooを呼び出す例。コンパイルできる。
struct ContentView: View {
var body: some View {
Text("Hello, World")
}
// それぞれから呼び出す非同期関数
func foo() async {}
func couldBeAnywhere() async {
let task = Task { @MainActor in
await foo()
}
await task.value
}
}
次にMainActor.run
から非同期関数fooを呼び出す例。これはコンパイルできない。
struct ContentView: View {
var body: some View {
Text("Hello, World")
}
// それぞれから呼び出す非同期関数
func foo() async {}
func mainActorRunSample() async {
await MainActor.run {
await foo()
}
}
}
理由はコメント欄をご確認ください。
感想
-
await MainActor.run {}
を使うのは- 同期関数のみを呼び出したい場合
参考
- hakingwithswiftの記事自体は、違いのあるなしということを論じてないが参考になった
Discussion
とありますが、
Task.init
のoperation
クロージャとMainActor.run
のbody
クロージャでは、定義をよく見てみるとasync
がついているかついていないかが異なっておりまして、MainActor.run
の方はクロージャに同期的な処理しか書けなさそうです🤔実際、出ているコンパイルエラーの、DeepL による和訳も、
であり、コードでも
await
のつかない処理を書くとコンパイルエラーが消えるようです🤔ありがとうございます。確かにコメントの通りですね。記事修正しておきます。
こちらこそありがとうございます!!
多分 MainActor.run のユースケースとしては、
@State
や@Published
の値を変更することを想定されているんじゃないかなと思います🤔また、非同期処理をメインスレッドで実行したい場合はその処理だけを @MainActor な別関数に切り出して、そのラップした関数を呼び出す方が、Task のキャンセル処理とかを考慮せずに済みそうで丸そうだなと思いました🤔