「TPL入門(16) - おわりに」で終わったはずの連載ですが、あれから読んだ記事にまだまだいろいろ書いてあったので、"落ち穂拾い" という形でもうしばらく紹介したいと思います。
落ち穂拾い第1回の今回は、TPLが出現する前までの並列処理の書き方についてです。TPLを使うと並列処理をスマートに記述できることは、すでにお分かりのことと思います。しかし、.NET Framework 4がリリースされる前に同じようなことを自力でしようとしたときには、苦労の割にあんまりイイことないです。そんな費用対効果が低かったという事実を実感して頂ければ、TPLの素晴らしさにより気付くと思います。
以前の記述方法
今回は特に簡単なfor文の並列化の例について取り上げてみます。まずはサンプルを見てください。
using System; using System.Linq; using System.Threading; namespace ConsoleApplication { class Program { static void Main() { int lower = 0; int upper = 10000; var source = Enumerable.Range(lower, upper).ToArray(); int chunk = (upper - lower) / Environment.ProcessorCount; var threads = new Thread[Environment.ProcessorCount]; for (int i = 0; i < threads.Length; i++) { int start = chunk * i + lower; int end = i < threads.Length - 1 ? start + chunk : upper; threads[i] = new Thread(() => { for (int j = start; j < end; j++) { //----- Do Something Console.WriteLine(source[j]); } }); } foreach (var thread in threads) thread.Start(); foreach (var thread in threads) thread.Join(); } } }
上のサンプルでは実行環境のCPU数に合わせてスレッドを立て、各スレッドにコレクション要素を等分で割り振って処理させています。一見上手くできているように見えるのですが、これを何度も繰り返すとなると毎回スレッドを生成して破棄してということになるので、オーバーヘッドが大きいです。もちろん、スレッドプールを利用すればより効率的になるかとは思います。さらに、CPUコアが1つしかない場合は逐次実行に切り替えないと、別スレッドが生成されるだけリソースの無駄になります。
また、あるスレッドは早く終了するかもしれませんし、あるスレッドは時間がかかるかもしれません。このようなときは負荷分散をする (早く処理が終わったスレッドが、時間がかかっているスレッドのタスクを代行するようにする) のが理想的です。上記サンプルでは等分割で処理を割り振っているため、そのような負荷分散が行われず、非効率であることは否めません。
最も、並列処理をしたいと思うたびにこのような書き方をするのは、とてもじゃないですがやってられません。
TPLを利用した記述方法
一方、TPLを利用した場合は次のように記述できます。
using System; using System.Linq; using System.Threading.Tasks; namespace ConsoleApplication { class Program { static void Main() { int lower = 0; int upper = 10000; var source = Enumerable.Range(lower, upper).ToArray(); Parallel.For(lower, upper, index => { //----- Do Something Console.WriteLine(source[index]); }); } } }
「並列に書きたい」という気持ちをストレートに表したコードになっており、雑念もなければ、旧来の書き方をした場合の欠点もありません。もう、TPLを使わない手はないですね!