カルボナーラ街道

計測と観察

Swift Concurrency メインスレッドクイズ(3) 1問

実行環境

> 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話題の記事を全て読んでいたら原因と対策が全て分かるだろう。復習としてどうぞ。

Question

gist.github.com

答えと解説

答え

true

解説

Detached Taskを使っているからactor contextは引き継がれないので非メインスレッドでしょ、と思いきやメインスレッドで実行される。

8行目の await self.heavyCall() の呼び出しは非メインスレッドで行われるが、heavyCall() の中身の処理はメインスレッドで実行される。heavyCall() はUIViewControllerのサブクラスが持つメソッドで、UIViewControllerはMainActorにisolatedされるためメソッドもMainActorにisolatedされる。処理順とスレッドの対応は以下。

  1. ViewController viewDidLoad(): MainActorにisolatedされているためメインスレッド
  2. ViewController viewDidLoad() Task.detached(): Detached Taskはactor contextを引き継がないため非メインスレッド
  3. ViewController heavyCall(): メソッド自体はMainActorのexecutor上で実行されるためメインスレッド

await キーワードはsuspension point

await というキーワードはただ単に非同期処理が行われるんだな、程度の理解だったけど、具体的には実行中のスレッドを放棄・譲ることを示すsuspension pointである。suspension pointであることを知っていれば、実際の処理がどのスレッドで呼ばれるかは呼び出し先の定義に左右されるということがイメージできる。await キーワードが使われる箇所自体は非同期処理が実行されるスレッドを指定しない(できない)。

developer.apple.com

forums.swift.org

メインスレッドをブロックしないためには

今回のコードは重い処理がメインスレッドで実行されるため、heavyCall() が終わるまでメインスレッドをブロックしてしまう。メインスレッド上で処理が実行されっぱなしだとUIが固まってしまう。UIが固まるということはユーザーの操作がアプリ上で不可能になってしまうためよろしくない。対応としては以下の3つがある。

1. 処理をnonisolatedにする

メソッドに nonisolated を付与してMainActorのisolatedを外す。

gist.github.com

2. 処理を他に移す

gist.github.com

パッと見、L18-19では非同期処理が無いのでDetached Taskは必要ないのでは?と思うが、heavyCall() は同期メソッドのため呼び出し元のスレッド上で実行される。つまり、Detached Taskに渡さなければメインスレッドで実行されてしまうのでやや注意。

3. Task.yield() を使ってスレッドを譲る

gist.github.com

今回のケースだと2が一番良いんじゃないかな。重い処理でメインスレッドに関係ない処理はMain Actorにisolatedされていないクラスや構造体で行えば良い。3は他と比べて処理に時間がかかった。スレッドを譲る処理はオーバーヘッドが結構ありそう、ということが分かって良かった。

  • ループの回数を10の6乗に変更した時のそれぞれにかかった時間
    • 1: 0.9秒
    • 2: 0.8秒
    • 3: 8秒

参考