Ver. 4.0
マルチコア CPU の普及に伴って、並列処理の重要性が増しています。 この時代背景に合わせるかのように、.NET Framework 4で並列処理用のライブラリが追加されました。
まずは、制御フロー( 「制御フロー」 参照)の並列化です。 Parallel クラス(System.Threading.Tasks 名前空間)を使うことで、 通常の for 文や foreach 文に非常に似た書き方で並列処理を行えます。
Parallel クラスは Invoke、For、ForEach の3つの静的メソッドを持っています。
表1: Parallel クラスを使った制御フローの並列化
| メソッド | 逐次処理版 | 並列処理版 |
|---|---|---|
| Invoke |
A(); B(); C(); |
Parallel.Invoke(A, B, C);
|
| For |
for (int i = 0; i < N; i++) { Console.WriteLine(i * i); } |
Parallel.For(0, N, i => { Console.WriteLine(i * i); }); |
| ForEach |
var data = Enumerable.Range(0, N); foreach (var x in data) { Console.WriteLine(x * x); } |
var data = Enumerable.Range(0, N); Parallel.ForEach(data, x => { Console.WriteLine(x * x); }); |
逐次処理とほとんど同じ書き方で並列処理ができます。
ただし、複数のスレッドから同じデータを読み書きする場合には排他制御が必要なので注意してください。 例えば、以下のような処理は、単に foreach 文を Parallel.ForEach メソッドに置き換えるだけでなく、 ロックが必要です。
var data = Enumerable.Range(0, N); var sum = 0; foreach (var x in data) { sum += x; } Console.WriteLine(sum);
以下のように、sum += x の部分にロックを掛けます。
var data = Enumerable.Range(0, N); var sum = 0; Parallel.ForEach(data, x => { lock (data) sum += x; }); Console.WriteLine(sum);
ロック自体がそれなりにオーバーヘッドのかかる処理なので、 この例の場合、並列化するとかえって遅くなる可能性があります。
LINQ に対する並列化の仕組みも用意されています。 System.Linq 名前空間に ParallelEnumerable というクラスが追加されていて、 このクラスで定義されている AsParallel 拡張メソッドを使えば、LINQ クエリを並列化できます。 (データ ソースに対して .AsParallel() を付けるだけです。)
var data = Enumerable.Range(0, N); var sqSum = data.AsParallel().Sum(x => x * x); Console.WriteLine(sqSum);
必要な排他制御は適宜ライブラリ内で行ってくれるので、 こちらはロックが不要です。 なので、データ ストリーム( 「ストリームとパイプライン」 参照)に対する並列処理は、 Parallel クラスを使うよりも、こちらを使う方がおすすめです。
ただし、多少の工夫が必要な場合もあります。 例えば、1つ前の要素を参照したいというような場合、 以下のように書いてしまいがちです。
// 1つ前の値を保存しておく var prev = data.First(); var max = int.MinValue; foreach (var x in data.Skip(1)) { // 階差の最大値 max = Math.Max(x - prev, max); prev = x; }
並列化したい場合、必ずしも順序の保証がないので、prev = x; では「1つ前の要素を保存」という処理になりません。 以下のような工夫が必要になります。
// 1項ずらしたデータ ストリームと Zip var difference = data.Zip(data.Skip(1), (i, j) => j - i); // そのあと、AsParallel var max = difference.AsParallel().Max();