実行環境
> swift --version swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4) Target: arm64-apple-macosx14.0 > xcodebuild -version Xcode 15.4 Build version 15F31d
モチベーション
コードを書いていたらメインスレッドがブロックされた挙動になったのでメモとして残す。このブログで書いてきたSwift Concurrency話題の記事を全て読んでいたら原因と対策が全て分かるだろう。復習としてどうぞ。
- Swift concurrency: Behind the scenes(WWDC21)を見ることでTask.yield()を使う必要性を理解できた - カルボナーラ街道
- Swift Concurrency メインスレッドクイズ(2) 2問 - カルボナーラ街道
- Swift Concurrency 何秒かかる?クイズ 3問 - カルボナーラ街道
- Swift Concurrency メインスレッドクイズ 5問 - カルボナーラ街道
Question
答えと解説
答え
true
解説
Detached Taskを使っているからactor contextは引き継がれないので非メインスレッドでしょ、と思いきやメインスレッドで実行される。
8行目の await self.heavyCall()
の呼び出しは非メインスレッドで行われるが、heavyCall()
の中身の処理はメインスレッドで実行される。heavyCall()
はUIViewControllerのサブクラスが持つメソッドで、UIViewControllerはMainActorにisolatedされるためメソッドもMainActorにisolatedされる。処理順とスレッドの対応は以下。
- ViewController viewDidLoad(): MainActorにisolatedされているためメインスレッド
- ViewController viewDidLoad() Task.detached(): Detached Taskはactor contextを引き継がないため非メインスレッド
- ViewController heavyCall(): メソッド自体はMainActorのexecutor上で実行されるためメインスレッド
await
キーワードはsuspension point
await
というキーワードはただ単に非同期処理が行われるんだな、程度の理解だったけど、具体的には実行中のスレッドを放棄・譲ることを示すsuspension pointである。suspension pointであることを知っていれば、実際の処理がどのスレッドで呼ばれるかは呼び出し先の定義に左右されるということがイメージできる。await
キーワードが使われる箇所自体は非同期処理が実行されるスレッドを指定しない(できない)。
メインスレッドをブロックしないためには
今回のコードは重い処理がメインスレッドで実行されるため、heavyCall()
が終わるまでメインスレッドをブロックしてしまう。メインスレッド上で処理が実行されっぱなしだとUIが固まってしまう。UIが固まるということはユーザーの操作がアプリ上で不可能になってしまうためよろしくない。対応としては以下の3つがある。
1. 処理をnonisolatedにする
メソッドに nonisolated
を付与してMainActorのisolatedを外す。
2. 処理を他に移す
パッと見、L18-19では非同期処理が無いのでDetached Taskは必要ないのでは?と思うが、heavyCall()
は同期メソッドのため呼び出し元のスレッド上で実行される。つまり、Detached Taskに渡さなければメインスレッドで実行されてしまうのでやや注意。
3. Task.yield()
を使ってスレッドを譲る
今回のケースだと2が一番良いんじゃないかな。重い処理でメインスレッドに関係ない処理はMain Actorにisolatedされていないクラスや構造体で行えば良い。3は他と比べて処理に時間がかかった。スレッドを譲る処理はオーバーヘッドが結構ありそう、ということが分かって良かった。
- ループの回数を10の6乗に変更した時のそれぞれにかかった時間
- 1: 0.9秒
- 2: 0.8秒
- 3: 8秒