マルチスレッドプログラミングの概要と、そのメリット・デメリットについて簡単にまとめます。
Unity2018.3.9 (C#の記事ですがUnityを使って動作確認しています)
マルチスレッドプログラミングとは?
まずマルチスレッドでないプログラムでは、1つのCPUコアで処理を先頭から順番に行っていきます。
CPUコアを一つしか持たないシングルコアのCPUではもちろん、
マルチコアのCPUにであっても一つのCPUだけを使って処理を行います。並列では行いません。
ここでマルチスレッドのプログラムを書くと、マルチコアCPUではスレッド毎に別のCPUコアで別の処理を行います。
シングルコアのCPUでは、一つのCPUコアで処理を行うものの、各スレッドを高速で切り替えながら少しずつ処理するため、擬似的な並列処理が実現されます。
効果1 処理時間が短縮される
マルチスレッドで処理を行うメリットの1つとして、マルチコアCPUで処理時間が短縮されることが挙げられます。
下図から明らかですが、マルチコアCPUでは使っていないCPUコアに処理を回せるので、全体としての処理時間は削減されます。
ただしシングルコアCPUではスレッドを切り替える処理の分だけ時間がかかるので、全体としての処理時間は逆に長くなります。
効果2 画面のフリーズを防ぐ
マルチスレッドには画面のフリーズを防ぐ効果もあります。
例えば非常に時間のかかる計算を処理するとき、メインスレッドでこれを行うと、
計算が完了するまで他の処理ができなくなるため画面がフリーズしてしまいます。
ここでこの計算処理を別スレッドに逃がすと、メインスレッドはその間に他の処理を行えるようになります。
つまり画面がフリーズしなくなります。
マルチスレッドで処理してみる
それでは実際にマルチスレッドで処理を行い効果を見てみます。
測定結果は手元のPC(6個のコア、12個のロジカルプロセッサ)によるものです。
まずはソースコードです。
検証のため直列で処理を行うメソッドと並列で処理を行うメソッドを用意し、それぞれの時間を測ります。
// 直列 async Task ProcessHeavyTasksSequential() { var stopwatch = new Stopwatch(); stopwatch.Start(); for (int i = 0; i < LOOP_COUNT; i++) { await HeavyTaskAsync(); } UnityEngine.Debug.Log(stopwatch.Elapsed); stopwatch.Stop(); } // 並列 async Task ProcessHeavyTasksParallel() { var stopwatch = new Stopwatch(); stopwatch.Start(); var tasks = new List<Task>(); for (int i = 0; i < LOOP_COUNT; i++) { tasks.Add(HeavyTaskAsync()); } await Task.WhenAll(tasks); UnityEngine.Debug.Log(stopwatch.Elapsed); stopwatch.Stop(); } async Task HeavyTaskAsync() { await Task.Run(() => { for (int i = 0; i < 1000000; i++) { var example = Mathf.Pow(2, 10); } }); }
測定結果は以下の通りです。
回数 | 直列処理時間(sec) | 並列処理時間(sec) |
---|---|---|
1回目 | 0.663 | 0.083 |
2回目 | 0.664 | 0.083 |
3回目 | 0.664 | 0.083 |
4回目 | 0.663 | 0.083 |
5回目 | 0.663 | 0.083 |
並列の処理のほうが圧倒的に早いことがわかりました。
効果があるのはあくまでCPUの処理
マルチスレッドプログラミングを行う際には、マルチスレッドが効果的に作用するのは
あくまでCPUの処理であるということに気を付けなければならないようです。
たとえばファイルIOはディスクが処理するので、並列化の恩恵はそこまで受けられないのではないかと思います。
これを検証してみます。
// 直列 async Task ReadFilesSequential() { var data = File.ReadAllBytes(FILE_PATH); var stopwatch = new Stopwatch(); stopwatch.Start(); for (int i = 0; i < LOOP_COUNT; i++) { await ReadFileAsync(FILE_PATH); } UnityEngine.Debug.Log(stopwatch.Elapsed); stopwatch.Stop(); } // 並列 async Task ReadFilesParallel() { var data = File.ReadAllBytes(FILE_PATH); var stopwatch = new Stopwatch(); stopwatch.Start(); var tasks = new List<Task>(); for (int i = 0; i < LOOP_COUNT; i++) { tasks.Add(ReadFileAsync(FILE_PATH)); } await Task.WhenAll(tasks); UnityEngine.Debug.Log(stopwatch.Elapsed); stopwatch.Stop(); } async Task ReadFileAsync(string filePath) { using (var fs = new FileStream(FILE_PATH, FileMode.Open, FileAccess.Read)) { try { var bs = new byte[fs.Length]; await fs.ReadAsync(bs, 0, bs.Length); } catch { } finally { if (fs != null) { fs.Close(); } } } }
測定結果は次の通りです。
回数 | 直列処理時間(sec) | 並列処理時間(sec) |
---|---|---|
1回目 | 0.740 | 0.762 |
2回目 | 0.737 | 0.655 |
3回目 | 0.740 | 0.638 |
4回目 | 0.740 | 0.603 |
5回目 | 0.745 | 0.600 |
並列のほうが若干早い(少なからずCPUで処理をしている分?)ものの、
前節と比べると明らかな優位性は認められませんでした。