LoginSignup
10

More than 1 year has passed since last update.

GlobalActorとTaskを使う際の実行スレッドがメインスレッドなのかを速攻で思い出すためのサンプルコード

Last updated at Posted at 2021-12-21

はじめに

みなさーん、ActorとTask使ってますかー?
 (ΩΩΩ<使ってまーす!!)

MainActorとかGlobalActor使ってる時にー、Taskの実行スレッドわけわからんくなりますよねー?
(Ω<ない気がするけどなあ。 Ω<あるようなないような。 Ω<それはないでーす!)

はい。みなさんわけわからなくなってますねー、ということで今日はGlobalActorとTask使ってる時にTaskの実行スレッドが想定通りかわからなくなったときに、すぐにそれが整理できるコード例を示したいと思います。

概要

コード例で示すことの概要

  • 型自体に対するActor isolated指定は型のメソッドに引き継がれる(他にはプロパティにも)
    • しかしメソッドに別のActor isolatedが指定してあれば指定した別のActor isolatedを優先する
      • 優先度は 型への指定 < メソッドへの指定
    • nonisolated指定されてると引き継がない
  • TaskクロージャのActor指定はそれが一番優先される
    • 優先度は 型への指定 < メソッドへの指定 < Taskクロージャへの指定
  • Task.detachedはどのActor isolatedでもなく構造化を引き継がない
    • スレッドはワーカースレッドになっている
    • Task { MainActor in }よりもTask.detached { (@)MainActor in ... }のほうが明確な気がするんだけど...
  • 継承先は継承元の型のActor isolatedを推論し従う
    • overrideするfuncはメソッドのActor isolatedを推論し従う
  • Actor isolated指定してないオブジェクトのTask { ... }は、ワーカースレッドが実行される
    • 推測
      • MainActorから暗黙的にTaskを引き継ぐ訳じゃないから妥当な気もする
      • 違う解釈の仕方
        • どのActor isolatedにも属さないと考える
          • 引き継ぎ元がTask.detached { ... }をしてるのと同じなのでワーカースレッド
            • それを引き継ぐようにワーカースレッドが選択実行される?と解釈してもいいのかも

コード

読み方の例

コピペで動くようにしておきました。assertは全てクラッシュしないようにしていてメッセージでその理由を書いています。

// Thread.isMainThreadはtrueを返すことを確認していてクラッシュしません。その理由がメッセージに。
assert(Thread.isMainThread, "単にメインスレッドから呼び出されりゃメインスレッド")
Task.detached(priority: .utility) {
    // !Thread.isMainThreadはtrueを返すことを確認していてクラッシュしません。その理由はメッセージに。
    assert(!Thread.isMainThread, "Task.detachedされてワーカースレッド")
}

実際のコード

SwiftUIからサンプル用のModelたちを呼び出しています。

import SwiftUI

@main
struct MyApp: App {
    @StateObject var model = Model()
    @StateObject var mainActorModel = MainActorModel()
    @StateObject var mainActorSubModel = MainActorSubModel()

    var body: some Scene {
        WindowGroup {
            ContentView(
                model: model,
                mainActorModel: mainActorModel,
                mainActorSubModel: mainActorSubModel
            )
        }
    }
}

struct ContentView: View {
    @ObservedObject var model: Model
    @ObservedObject var mainActorModel: MainActorModel
    @ObservedObject var mainActorSubModel: MainActorSubModel

    var body: some View {
        VStack {
            Text("Hello, world!")
        }
        .task {
            await model.foo()
            await model.bar()
            await model.baz()

            await mainActorModel.foo()
            await mainActorModel.bar()
            await mainActorModel.baz()
            await mainActorModel.hoge()

            await mainActorSubModel.foo()
            await mainActorSubModel.bar()
            await mainActorSubModel.piyo()
        }
    }
}

class Model: ObservableObject {
    func foo() async {
        assert(Thread.isMainThread, "単にメインスレッドから呼び出されりゃメインスレッド")

        Task {
            assert(!Thread.isMainThread, "Taskはワーカースレッド。おそらく引き継ぐActorがないから?")
            print("model\(#function)")
        }
    }

    func bar() async {
        assert(Thread.isMainThread, "単にメインスレッドから呼び出されりゃメインスレッド")

        Task { @MainActor in
            assert(Thread.isMainThread, "TaskはMainActor指定されているのでメインスレッド")
            print("model.\(#function)")

            Task.detached(priority: .utility) {
                assert(!Thread.isMainThread, "Task.detachedされてワーカースレッド")
            }
        }
    }

    @MyActor
    func baz() async {
        assert(!Thread.isMainThread, "メソッドがSubActor指定されてるのでワーカースレッド")

        Task {
            assert(!Thread.isMainThread, "TaskはメソッドがSubActor指定されてるのでワーカースレッド")
            print("model.\(#function)")

            Task { @MainActor in
                // Task.detached { @MainActor in ... } のほうが明確な気がしないでもない。
                // 結局MainActorに切り替えてるんだから。
                // そもそもDispatchQueue.main.async みたいなあんまりこういうレガシーなことやりたくない。最後の手段。
                assert(Thread.isMainThread, "TaskはMainActor指定されているのでメインスレッド")
            }
        }
    }
}

@MainActor
class MainActorModel: ObservableObject {
    func foo() async {
        assert(Thread.isMainThread, "MainActorだしメインスレッド")

        Task {
            assert(Thread.isMainThread, "Taskは暗黙的に所属Actorに従うのでメインスレッド")
            print("mainActorModel.\(#function)")

            Task.detached(priority: .utility) {
                assert(!Thread.isMainThread, "Task.detachedされてワーカースレッド")
            }
        }
    }

    @MyActor
    func bar() async {
        Task {
            // つまり 型のActor < メソッドの指定Actor ってコト?
            assert(!Thread.isMainThread, "TaskはSubActor指定されてるのでワーカースレッド")
            print("mainActorModel.\(#function)")
        }
    }

    @MyActor
    func baz() async {
        Task { @MainActor in
            // つまり 型のActor < メソッドの指定Actor < Taskの指定Actor ってコト?
            assert(Thread.isMainThread, "TaskはMainActor指定されてるのでメインスレッド")
            print("mainActorModel.\(#function)")
        }
    }

    nonisolated func hoge() async {
        assert(Thread.isMainThread, "実行スレッドになるだけなのでメインスレッド")
        Task {
            assert(!Thread.isMainThread, "ワーカースレッド。nonisolatedされてるのでMainActorを引き継がない")
        }
    }
}

class MainActorSubModel: MainActorModel {
    override func foo() async {
        assert(Thread.isMainThread, "継承元がMainActorだしメインスレッド")
        print("mainActorSubModel.\(#function)")
    }

    override func bar() async {
        assert(!Thread.isMainThread, "override元のメソッドに従ってワーカースレッド")
        print("mainActorSubModel.\(#function)")
    }

    func piyo() async {
        Task {
            assert(Thread.isMainThread, "Taskは暗黙的に所属Actorに従うのでメインスレッド")
            print("mainActorSubModel.\(#function)")
        }
    }
}

@globalActor
struct MyActor {
    actor Sample { }
    static var shared = Sample()
}

その他

Fruta

GlobalActorとTaskを使ったAppleのサンプルコードFrutaを参考にどうやってるかを見る

ObservableObjectにMainActor指定していない件

  • Model: ObservableObjectをMainActor指定してない
    • Publishedを別スレッドから更新したら警告出るような気がするので必要な箇所MainActor指定するほうがいいという判断?

Task { @MainActor in }の件

  • Task { @MainActor in ... }使ってて Task.detached { @MainActor in ... }ではない
    • Task { @MainActor in ... }使ってると
      • func purchase(product: Product)はメインスレッドで呼び出されると
        • Task { @MainActor in }はメインスレッドのまま動作する
    • Task.detached { @MainActor in ... }にしてると
      • func purchase(product: Product)はメインスレッドで呼び出されると
        • メインスレッドからメインスレッドに切り替える際にスレッドの呼び出しに無駄がある?
      • もしくはもし呼び出し元が別のActorだったらその構造を引き継げるから?
class Model: ObservableObject {
...
   func purchase(product: Product) {
        Task { @MainActor in
            do {
                let result = try await product.purchase()
                guard case .success(.verified(let transaction)) = result,
                      transaction.productID == Model.unlockAllRecipesIdentifier else {
                    return
                }
                self.allRecipesUnlocked = true
            } catch {
                print("Failed to purchase \(product.id): \(error)")
            }
        }
    }

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
10