概要
Unity 非同期完全に理解した勉強会 - connpass の講演動画を見てまとめた物。
結構前の物だが、動画を見て参考になったので必要そうな情報をまとめて自分用にメモした。
UniRxについての記事は toRisouP - Qiita さんの記事を見たほうが分かりやすい。
まとめ
- 非同期でもマルチスレッドとは限らない
- マルチスレッドはUniTaskを使った方が早い
- 通常のコルーチンよりもMicroCotoutineの方がパフォーマンスが良い (yield return nullのみ)
- UniRxやasync/awaitは非同期処理を書きやすくする為の物
参考動画や記事
- はたらくスレッド - YouTube
- Deep Dive async/await in Unity with UniTask(UniRx.Async) - YouTube
- Observable の非同期処理への活用 - YouTube
- async/await のしくみ - YouTube
- neue cc - Unityにおけるコルーチンの省メモリと高速化について、或いはUniRx 5.3.0でのその反映
非同期処理の基礎
特徴
- 結果をすぐに返さない処理
- 関数の終了を終わるのを待たずに次の処理が出来る
- 非同期処理は必ずしもマルチスレッドとは限らない
- コルーチンはシングルスレッドで非同期処理してる
- 同時実行と並列実行があり、同時はCPU共有、並列はCPUごとにスレッド実
- 処理が終わった時にメッセージを発行
- メッセージを受け取った側が何かしらの処理をする
- 発行されるメッセージはOnNextかErrorのどちらか一つ
問題点
- 実行結果の取り扱い
- 直列/平行処理
- エラーハンドリング
- 実行キャンセル
- 実行コンテキストの制御 (マルチスレッド時の制御)
- 愚直に書くと手に負えない
エラーハンドリング
- ログに出して終わり
- リトライする
- 別の処理にフォールバックする
- 処理を諦める
- 失敗を握りつぶす
.NETの非同期
- Thread, OSのスレッドと1対1で対応、重い
- ThreadPool, 最初にスレッドを立てておいて使い回す, キューにタスクを貯める
- Task, 非同期処理の続きが書ける, ただ分岐やループを書くのが辛い
- Taskとawaitを使うとコールバック地獄を防げる (Task以外にも出来る)
Unityで非同期を扱う仕組み
- Unityコルーチン (メインスレッド, 結果は返さない)
- async/await + Task (C#, .NET 4.x)
- UniRx (Observable) (Taskの拡張版、Operator, Schedulerが便利)
- UniRx (UniRx.Async) (新しい機能, Async/Await を Unity向けに最適化した物)
- 独自実装
async/awaitとは
- async/await 非同期を同期的に扱う仕組み
- async/await はマルチスレッドではない
- async/awaitはCPS変換、実装の詳細でコルーチンが選ばれているだけ
- await 処理は一旦別スレッドに制御を移し、完了後に続きの処理を再開する
- await 内処理ではフレームに依存しない (コルーチンと似たようなもの)
- await Task.Run以降のラムダ式をメインスレッドとは別スレッドで実行可能
スレッドの基礎
- スレッドは処理の単位
- スレッドごとにスタックとレジスタがある (リソースやコンテキストと呼ぶ)
- CPUコア1つでマルチスレッドをする時、スレッドのリソースを切り替える (コンテキストスイッチ)
- マルチスレッドで動作しても問題無い処理をスレッドセーフと呼ぶ
- スレッド立ち上げは重い、そこで予めスレッドを立てて使い回す仕組みをスレッドプールと呼ぶ
- 同期コンテキスト(SynchronizationContext)を使うと、元スレッドに戻る時に文脈を戻すことが出来る
並列プログラミングとは
- 非同期処理とはまた別のもの
- 比較的短い処理を複数スレッド使って同時に実行
- マルチコアな物理ハードウェアのパフォーマンスを最大限に引き出す
- C#JobSystemを使うことで実装可能
- C#Parallelを使うとより簡単に実装出来る
- 通信やI/O待ちが発生するものには向いていない
Unityの非同期処理の仕組み
- Unityは概ねシングルスレッド
- C++のエンジンレイヤーにC#のスクリプティングレイヤーが載ってる
- C#側の扱いはほぼシングルスレッド
- Taskのasync/awaitはすぐにスレッドプールに飛ばす
UniTaskとは
- UniRx.Async の主要クラス (Unity 2018.3から利用可能)
- IncrementalCompiler を利用することでも利用可能
- Unity が特殊な実行環境なので特化させることで最速を実現する
- UniTaskを使えばパフォーマンスが高い
- 標準のTaskは使わない方が良い
- コルーチンを置き換えるためのユーティリティ
- 性能のための UniTask + async/await
- UniTask Tracker によりリークを回避可能
- TaskやRxと一緒にも使える
Rxとは
- Rxは時間を軸に取ってコレクション操作に見立てて統一的に扱える
- Rxはイベントストリームの活用に使っていこう
- Unity向けRxをUniRxと呼ぶ
Rxをイベントでは無く非同期として扱う
- イベント処理だけじゃなくて非同期にも使える
- メッセージ処理のオブジェクトでストリームのこと
- MessageSource + Operator + Schedulerで構成される
- イベント処理も非同期処理の両方でObservableを使え
- Hot/Coldの扱いに気をつける
UniRxの機能
- ContinueWith, Observableを直列にする時に使うOperator
- OnErrorRetry, TimeOut等のOperatorで簡単にエラーハンドリング出来る
- AsyncSubject, 非同期処理向けSubject, メッセージを1個だけキャッシュ出来る
- PublishLast, Hot変換用Operator, AsyncSubjectを使って事前にObservableをSubscribeする
- Observable.Start, Task.Run と同じ機能, 同期処理をマルチスレッドの非同期処理に変換可能
- Observable.Create, 任意の手続きから任意のObservableを作れる, 独自非同期処理をラップ出来る
- Observable.FromCoroutine, コルーチンから任意のObservableを作れ、結果を取り出せるようになる
- ToYieldInstruction, コルーチン上でObservableの完了待ちが可能、async/awaitっぽく書ける
コルーチンと比較
- プリミティブに近く、それほど機能が多くない
- Observable.FromCroutineを使うと、中身コルーチンでガワをObservableに出来る
- コルーチンは結果が返せない、Observable は変数に代入して取り出せる
- コルーチンで直列処理を入れるとコールバック地獄に陥る
- Observable は ContinueWith でチェーンで綺麗に書ける
- コルーチンは平行で結果を待ち受ける機能は無い
- Observable は WhenAll で包むだけで良い
- Observable を使ったほうが結果の取り扱いも結合も楽になる
Subjectとは
- メッセージ発行の根源になるオブジェクト
- Subscribe する前のオブジェクトを受け取ることは出来ない
Observableの性質
- Subscribeされる前は放置される (ColdObservable)
- Subscribeされると初めてインスタンス化される
- 既に可動済みのObservableはHotObservableと呼ぶ
- LINQはforeach呼ぶまで評価しない (LINQとRxは似ている)
まとめ
雑で申し訳ないですが、詳細は動画を見るのが早いと思われます。
間違い等があればコメントを頂けると幸いです。