メソッドがMainActorでも別スレッドで動作することはある
背景
Swift 5.5あたりからUIViewControllerはMainActorとなっていて、自作する型にもglobalActorを設定することが当たり前になってきた。そんななか、リリース済みのアプリでの不具合を調査する際に、「これMainActorなんだから別スレッドで動作してるってこたーねーよな」という思い込みで可能性を排除していたが、そんなことはない。という話を書いておきます。
私の勘違いかもしれないのでもしなんかあればコメントをお願いします。
要点
メソッドにMainActorを設定しても、非asyncなメソッド はレガシーなDispatchQueue.global()
など別スレッドを利用するキューから実行すれば、それはその任意の別スレッドでメソッドを実行させられる。
つまりTaskを代表とするSwift Concurrencyだけを使っている場合はMainActor指定されたメソッドはメインスレッドで実行される。しかし、レガシーな方法ではglobalActorなどの制約に対してあまり意味がない。
結論として言いたいこと
- なるべく新規のコードではSwift Concurrencyを使ってほしい
- Swift Concurrencyを使うことで、コンパイラの指摘に従うようにしてほしい
- つまり、レガシーな方法であるDispatchQueue, Operation, Threadを使わないでほしい
- どうしても使う場合はprivateなメソッド内で行い、Swift Conccurencyで扱えるように変換してほしい
- 正直、OSSライブラリでスレッドが切り替わるものも、Taskに変換しないで使うのはコンパイラの助けが受けられないので良いとは 私は 思わない
- どうしても使う場合はprivateなメソッド内で行い、Swift Conccurencyで扱えるように変換してほしい
例
MainActorでも別スレッドで動くよね、な具体例をあげる
ViewControllerからMainActorを呼び出す
- MyViewControllerからMainActor指定してあるCounterのメソッドを呼び出す
- DispatchQueue.global()でグローバルキューから実行
- Counterのメソッドは別スレッドで呼び出されてしまう
- DispatchQueue.global()でグローバルキューから実行
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 下記はもちろんMainActorじゃないのでコンパイルエラー。
// 我々はコンパイラの能力を享受できる。
// Task.detached {
// let counter = Counter()
// counter.increment()
// }
// 下記はメインスレッド外だがコンパイルエラーにはならない
DispatchQueue.global().async {
let counter = Counter()
counter.increment()
}
}
}
@MainActor
class Counter {
// 暗黙的にMainActor
var count = 0 {
didSet {
print("didSet", Thread.isMainThread) // => false
}
}
// 暗黙的にMainActorだが非asyncメソッド
func increment() {
// DispatchQueue.main.asyncなどで呼ぶとメインスレッドで動作しない
print("viewDidLoad", Thread.isMainThread) // => false
count += 1
}
}
let viewController = MyViewController()
PlaygroundPage.current.liveView = viewController
本来MainActor指定されたCounterはメインスレッドでしか操作されない。そう思って定義を眺めていても、実際に呼び出す側の都合によって変わる。
UIViewControllerもMainActorだが、OSSライブラリからコールバックなどで非メインスレッドから呼び出された場合にはそのスレッドで動作してしまう。
警告もしくはエラーについて
TODO: あとで追記。コンパイラフラグを追加してDispatchQueue.global().async
から呼び出されづらいようにするんや!
参考文献
型にglobalActor指定
型にglobalActorを指定すると暗黙的にプロパティやメソッドにそれが引き継がれる件についてはプロポーザルに書いてある。
プロポーザル
CofeeData
WWDC21でのAppleのサンプルで型にMainActorを指定してる箇所のはなし
Put the Coffee Data Class on the Main Actor
The CoffeeData class implements ObservableObject and has an @Published property to feed the SwiftUI views. To ensure that all updates to this property are made on the main thread, place the type on the main actor:
話が脱線しそう
UIViewControllerがMainActorな件
UIViewControllerがMainActorな件は下記の公式リファレンスに書いてあると教えていただいた
蛇足
- Xcode 14より前はSwiftのヘッダーみても書いてなかったが、Xcode 14からはSwiftのヘッダーにもMainActorが書かれてる
- Xcodeのエディタ上でUIViewControllerをJump to Definitionしたらわかる
その他
- WWDCですでに話されてたり、当たり前のことかもしれない
- 解釈を間違われないように念の為
- DispatchQueue.globalをすべて滅ぼせというわけじゃなくしょうがないときは変換すればいいと思う
- 中途半端に移行するのは良くない、なんて言ってない
Discussion