はじめに
みなさーん、ActorとTask使ってますかー?
(ΩΩΩ<使ってまーす!!)
MainActorとかGlobalActor使ってる時にー、Taskの実行スレッドわけわからんくなりますよねー?
(Ω<ない気がするけどなあ。 Ω<あるようなないような。 Ω<それはないでーす!)
はい。みなさんわけわからなくなってますねー、ということで今日はGlobalActorとTask使ってる時にTaskの実行スレッドが想定通りかわからなくなったときに、すぐにそれが整理できるコード例を示したいと思います。
概要
コード例で示すことの概要
- 型自体に対するActor isolated指定は型のメソッドに引き継がれる(他にはプロパティにも)
- しかしメソッドに別のActor isolatedが指定してあれば指定した別のActor isolatedを優先する
- 優先度は
型への指定 < メソッドへの指定
- 優先度は
- nonisolated指定されてると引き継がない
- しかしメソッドに別のActor isolatedが指定してあれば指定した別のActor isolatedを優先する
- 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 { ... }
をしてるのと同じなのでワーカースレッド- それを引き継ぐようにワーカースレッドが選択実行される?と解釈してもいいのかも
- 引き継ぎ元が
- どのActor isolatedにも属さないと考える
- 推測
コード
読み方の例
コピペで動くようにしておきました。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)")
}
}
}