Swift concurrency: Behind the scenes(WWDC21)を見ることでTask.yield()を使う必要性を理解できた

要約

今まで Task.yield() を積極的に使わなくても特に問題は無いと思っていたけど、以下の考えに落ち着いて積極的に使おうという考えに変わった。

  • Swift ConcurrencyではCPUコアと同数のスレッドが作成される
  • タスクが完了するか、 Task.yield() を使ってスレッドを明け渡さないと、優先度が高いタスクを即座に実行できない可能性があるため、重い処理を扱うループ内では Task.yield() を呼ぶ

Task.yield()

developer.apple.com

実行中のタスクを一時停止するAPI。他に優先度が高いタスクがあればそちらが実行され、そうでなければ再開される。

いつ使うのかがピンと来ていない

使うタイミングがピンと来ていなかったので自分の考えを整理した。

メインスレッド

そもそもメインスレッドに重い処理を実行させると、iOSアプリ文脈においてはUIの応答性低下につながるため、重い処理は実行させたくない。複数の重いUI描画処理を実行させたい場合のループ毎に Task.yield() を呼ぶ程度。あまり出番は無い認識。

非メインスレッド

非メインスレッドにおいては Task.yield() を呼ぶ必要ないだろう。というのも、アプリケーションが実行される昨今の環境におけるリソースは潤沢だから。広義モバイルの文脈においては、Apple Watchですら2コアあり、iPhoneは6コア。複数のアプリケーションが端末上で動作しているとはいえ、アプリケーション開発者がスレッド周りのリソースを管理しなければならないほどリソースは枯渇しないだろう。

APIが用意されているということは理由があるのかも

自分の考えを書いたが、思惑無くしてAPIが用意されることはあるのだろうか、いやないと思い、仮説を立てて調べた。

  • リソース制約が厳しい環境でSwiftが使われていく説
  • 自分のSwift Concurrencyの理解が甘い説

リソース制約が厳しい環境でSwiftが使われていく説

最近では以下のような組み込み文脈でSwiftが使われる事例が出ている。リソースがiPhoneなどのモバイル端末よりも厳しい環境下での動作を見越してAPIが提供されている説を考えた。

www.swift.org

しかし、記事中のapple/swift-embedded-examplesでTaskをサーチしても引っかかからずだった。

https://github.com/search?q=repo%3Aapple%2Fswift-embedded-examples%20Task&type=code

examplesレベルでまだ実践的な内容が無いからなのか、そもそも非同期処理が組み込みでそれほど必要とされないのかの判断がつかなかった。

自分のSwift Concurrencyの理解が甘い説

最初に疑うはこちらだった。結論としてはこっち。Swift Concurrency関係のWWDCの動画やswift-evolutionを全て見ていないなと思って、ひたすら見ていくと、以下の動画を見たら自分の考えが固まった。

developer.apple.com

この動画によると、Swift Concurrency以前の非同期処理を提供するGCD(Grand Central Dispatch)では、CPUコア数よりも多い数のスレッドを扱えること、スレッドが多すぎるとコンテキスト切り替えによるパフォーマンス低下が課題としてあった。一方でSwift ConcurrencyではCPUコア数と同数のスレッドのみの作成で非同期処理を実現している。CPUコア数分のスレッド上でタスクが実行されている際に、優先度が高いタスクに処理をさせるには実行中のタスクが完了するか、Task.yield() で譲る他ないため、積極的に Task.yield() を使っていこうと思えた。優先度が高いタスクが生まれたからといって勝手にスレッド上で実行するタスクを組み替えてくれるわけではないのだな。

初見ではスレッド周りの知識が無くて理解ができず、動画を最後まで見ることができなかった。対応として、以下の5章まで読んでプロセスやスレッドの理解を深めてから再度動画を見ると概ね言わんとしていることは理解できた。

gihyo.jp

また、以下の動画も考えを固める上で参考になった。

developer.apple.com

13:36あたりで、SwiftUI文脈でObservableObjectを使う際に @MainActor を付与すると、コンパイラがそのプロパティやメソッドがmain actorからのみアクセスされることを保証することが紹介されている。裏を返すと、@MainActor が付与されたクラスはデフォルトで処理がメインスレッド上で行われるため、メインスレッドで行わなくて良い処理は適宜別のactorにisolatedさせるなどをした方が良いなと思った。自分もよく使うパターンだったけど、重い処理をmain actorから逃すことまでの意識はそれほど無かった。成り行きに任せていた。

以上を踏まえた Task.yield() を使うタイミング

実装時に明にスレッドを明け渡す処理を書かないと、優先度の高いタスクが来たとしても他のタスクの実行状況によっては即座に実行できない場合があるため、重い処理を実行させる時はループ毎に Task.yield() を呼んでスレッドを明け渡す余地を作る。メインスレッドだろうが非メインスレッドだろうが関係なく。

参考