🧵

await MainActor.run { ... }とawait Task { (at)MainActor in ... }の違い

2022/08/29に公開
3

はじめに

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 {}を使うのは
    • 同期関数のみを呼び出したい場合

参考

https://www.hackingwithswift.com/quick-start/concurrency/how-to-use-mainactor-to-run-code-on-the-main-queue

  • hakingwithswiftの記事自体は、違いのあるなしということを論じてないが参考になった

Discussion

UhucreamUhucream

MainActor.runのbodyクロージャは
@MainActor @Sendable () thorws -> T
つまり、クロージャ内では@MainActorなものしか入れられない(?)

とありますが、Task.initoperation クロージャと MainActor.runbody クロージャでは、定義をよく見てみると async がついているかついていないかが異なっておりまして、MainActor.run の方はクロージャに同期的な処理しか書けなさそうです🤔


実際、出ているコンパイルエラーの、DeepL による和訳も、

error: cannot pass function of type '@Sendable () async -> ()' to parameter expecting synchronous function type

error: 同期関数型を期待するパラメータに、'@Sendable () async -> ()' 型の関数を渡すことはできません。

であり、コードでも await のつかない処理を書くとコンパイルエラーが消えるようです🤔


yimajoyimajo

ありがとうございます。確かにコメントの通りですね。記事修正しておきます。

UhucreamUhucream

こちらこそありがとうございます!!

多分 MainActor.run のユースケースとしては、@State@Published の値を変更することを想定されているんじゃないかなと思います🤔

また、非同期処理をメインスレッドで実行したい場合はその処理だけを @MainActor な別関数に切り出して、そのラップした関数を呼び出す方が、Task のキャンセル処理とかを考慮せずに済みそうで丸そうだなと思いました🤔