他言語から入ると、一見取っつきづらいDispatchQueue
ですが、だいぶ浸透してきた気がします。
一方で、簡単にマルチスレッドで非同期処理ができるようになった結果、処理フローの制御がしづらくなったり、
知らないうちに、CPUやメモリリソースを異常に消費したりなど、副次的な問題が出てくることがあります。
それらの問題に対する、解決策を考えてみます。
TL;DR
半分くらい当時のチームへの愚痴で自分で読み返して何が言いたいかわからなかったので、当時の気持ちを思い出しながら言いたいことベスト3を加筆しました。
- UIの更新など必要な場所以外では
main.async
やめよう、マジで - 時間のかかる処理はサブスレッドをうまく活用しよう
- 何でもかんでも非同期処理(特に並列処理)に回してるとメンテナンスコストかかるぜ!
GCD復習
Grand Central Disptach(GCD)
は、従来のマルチスレッドのAPIとは異なり、Dispatch Queue
というキューによって、OSが適切な優先度や実行スレッドを決めて、タスクを実行する仕組みです。
開発者は、実行したいブロックを、DispatchQueue
に追加するだけで、細かいスレッドの区分を意識することなく、メインスレッドとサブスレッドの区別だけで、並列処理や非同期処理を書くことができます。
問題
ここで生じる問題は、
async
の使いすぎによって、非同期処理の実行や完了の流れが制御しきれなくなる- 実行のタイミングの制御が見えないので、過剰にリソースを使う処理を書いても気づかない場合がある
ということです。
(実際、ライブラリに依存せずDispatchQueue
を使っているチームや、RxSwiftをメインに使っているチームでも、メインスレッドで非同期処理が走りまくりパフォーマンスが落ちるというケースはありました。)
方針
本当にasyncが必要なのか考える
まず、非同期な並列処理は、処理が複雑になり完了のタイミングや順番は事前に予測しづらいので、いつどのような順番で終わってもいいような処理を書かなくてはなりません。
これは、上記AppleのDefine Your Application’s Expected Behavior
のセクションにも書かれています。
Before you even think about adding concurrency to your application, you should always start by defining what you deem to be the correct behavior of your application. Understanding your application’s expected behavior gives you a way to validate your design later. It should also give you some idea of the expected performance benefits you might receive by introducing concurrency.
非同期の並列処理を増やせば増やすほど、処理の完了の順番の組み合わせが増え、正しい振る舞いを気にしたプログラムやテストを書かなくてはなりません。
どうしてもパフォーマンス的にasync
でなければいけない処理以外は、同期処理や直列処理の方が実装、運用コストも減るはずです。
非同期処理自体の数を減らしたりサブスレッド上の直列処理にすることで、結果を利用するコードもシンプルになります。
main.asyncに気をつける
iOSではUIの処理は、メインスレッドで実行しなくてはなりません。
逆にそれ以外の処理で、DispatchQueue.main.async
を使うと、UIの処理をブロックしてしまうことがあります。
したがって、サブスレッドのタスク内でのUIの更新などを除き、本当に限られた場面でしか、わざわざmain.async
を使う場面はないはずです。
サブスレッドのタスク内でUIの更新を内包している場合は、そこだけメインスレッド(メインキュー)で行なったほうが良いです。 それ以外の重い処理は、グローバルキューなどで行います。
DispatchQueue.global(qos: .userInitiated).async { // UI以外の処理... DispatchQueue.main.async { // UIの更新(それ以外は書かない) } }
ここを適当にしてしまうと、意図せずUIの処理が重くなり、アプリの見栄えも悪くなります。
ライブラリを使っている場合、暗黙のうちに使ってしまっている場合もあります。
QoSを指定する
ブラックボックスの部分なので、どのくらい効果があるのかわかりませんが、グローバルキューのQoS(Quality of Service)はきちんと指定しておきましょう。
QualityOfService - Foundation | Apple Developer Documentation
指定できるものとしては、4種類あります。
優先度 | QoS | 説明 | 例 | Int値 |
---|---|---|---|---|
高 | userInteractive |
ユーザーにただちに処理の結果を返す必要がある | UI用の計算 | 33 |
userInitiated |
ユーザーに数秒以内に処理の結果を返す必要がある | メールのフェッチ | 25 | |
utility |
数十秒〜数分に一度動かすくらい | 定期的なリスト更新 | 17 | |
低 | background |
終わらなくてもユーザーが気にならない | バックアップ処理 | 9 |
global
キューや、定義したキューを使う際は、指定しない場合default
となってしまいます。
特に急ぎでない処理は、utility
やbackground
を指定しましょう。
DispatchQueue.global(qos: .utility).async // ついでにラベルもつけよう let queue = DispatchQueue(label: "com.hogehoge.fuga", qos: .utility)
DispatchGroupも使う
DispatchGroup
を使うことで、複数のキューのタスクが全て終わるのを検知することができます。
どうしてもキューが立ちすぎた時は、できるだけまとめましょう。
DispatchGroup - Dispatch | Apple Developer Documentation
let dispatchGroup = DispatchGroup() let queue = DispatchQueue(label: "com.hogehoge.fuga", qos: .userInteractive) let anotherQueue = DispatchQueue(label: "com.hogehoge.foo", qos: .userInteractive) queue.async(group: dispatchGroup) { // 処理 } anotherQueue.async(group: dispatchGroup) { // 処理 } dispatchGroup.notify(queue: .main) { // タスク完了後の処理(mainキューで実行) }
ダメそうだったら
どうしてもダメそうだったら、各種非同期ライブラリの導入を検討した方がいいかもしれません。
BoltsやHydraは直列処理が多い部分に強いです。逆にRxSwiftは複数の並列処理の結果を組み合わせるのに強いです。
中の実装はDispatchQueue
に依存しているものが多いので、メインスレッドとサブスレッドをしっかり使い分ける必要があります。
ただ、ライブラリは記述の助けにしかならないので、処理を非同期にするかどうかはしっかり吟味した上で、設計はした方が良いと思います。(膨大な負債になります😭)