Unityのマルチスレッド界隈のはなし
Unityでのマルチスレッド界隈について断片化した理解にとどまっていたので、自分なりのまとめを行ってみました。
まず非同期実装と並列プログラミングは分けて考えよう
- 非同期実装は順番通りに処理を実行しない、逐次処理とは対比的な処理のことである
- 非同期処理は通信などの重い処理を別スレッドに逃しメインスレッドを専有させないことを目的としている
- 一方で、並列実装は比較的短い(1フレーム内で収まる)処理を複数のスレッド使って同時に実行することでマルチコアな物理ハードウェアのパフォーマンスを最大限に引き出そうというアプローチである
- 非同期実装に適しているのがawait/async、Taskを使った方法である
- 並列プログラミングの実装に適しているのがC#のParallelクラス、Unityが提供するC#JobSystemになる
C#JobSystem
- 元々、UnityエンジンではWorkerThreadをコア数-1(例外あり)で持っていた、それをユーザーにも開放した形になる
- 1フレームで収まる処理を並行的に行うのに適している
- 通信やI/O待ちが発生する非同期的な処理には向かない
- その場合には、後述するawait、Taskを使っての実装を行うのがよい
- 通信やI/O待ちが発生する非同期的な処理には向かない
- イテレーションを回すコレクションはC#のコレクションではなくUnityが独自に用意したNativeArrayというunsafeなコレクションになる
- C#のコレクションはメモリ上に離散的に存在するのでキャッシュヒット率がとても悪い、JobSystemではNativetiveArrayを使用することでメモリ上に連続的に展開を行いキャッシュヒット率を上げる
- そのためintやstructなどのプリミティブ型に限られる
- メインスレッド制約のあるUnityの通常のAPIは使えない
- MainThreadで実行してくださいというエラーが出る
- JobSystem上で動く専用のAPIが順次増えていく予定とのこと
- JobSystem内で戻り値を返す方法がやや煩雑な印象である
- 詳しい使い方はこちらのブログ記事を参考にすると良いと思います
C#での非同期実装
- C#5でasync/awaitというキーワードが導入された
- awaitは文字通り「待つ」の仕組みを提供するものである
var task = SampleAsync(); // 実行 Debug.Log("Immediate Execution"); <- 即時実行される await task; Debug.Log("SampleAsync() compleated!"); SampleAsync()の実行完了を待って実行される
- awaitにTaskクラスを組み合わせて使うことで 非同期実装が容易に実現できる
- Taskは一連の処理をまとめるためのものである。1つのTaskをメインではない別スレッドで丸っと実行させるということも簡単に実現できる
- awaitは特定の条件を満たしたクラスに対して使うことができ、Taskがその条件を満たした実装になっているので使用が可能となっている
- 詳しいTaskの使い方はこちらを参考にしてください
- 条件を満たすように実装を行えばawaitをTask以外にも利用でき、例えばUnityコルーチンをawaitさせるということも可能である
- WebHttpRequestなどは非同期実装が行いやすいようにTaskクラスを返してくれる非同期の通信メソッドがあらかじめ用意されている
var webReq = (HttpWebRequest)WebRequest.Create(URL); await webReq.GetResponseAsync();
- 非同期用のメソッドにはxxxAsyncと命名するのが通例である
- 詳しくはこちらを参考にしてください
C#Parallelクラス
- 並行プログラミングを簡単に実装できる仕組みである
- 立ち位置的にはC#Jobsystemとオーバーラップする
- UnityではC#JobSystemのほうがパフォーマンスは出るのでしょうが、簡単に記述できるという利点がこちらにはある
Parallel.For(0, N, i => { Debug.Log(i); // 複数のスレッドで同時並行的に実行する });
参考
ここで書いたC#JobSystemの情報については、こちらの勉強会に参加して得た情報になります。