概要
Ver. 4.0
マルチコア CPU の普及に伴って、並列処理の重要性が増しています。 この時代背景に合わせるかのように、.NET Framework 4で並列処理用のライブラリが追加されました。
Parallel クラス
まずは、制御フロー(「制御フロー」参照)の並列化です。 Parallel クラス(System.Threading.Tasks 名前空間)を使うことで、 通常の for 文や foreach 文に非常に似た書き方で並列処理を行えます。
Parallel クラスは Invoke、For、ForEach の3つの静的メソッドを持っています。
メソッド | 逐次処理版 | 並列処理版 |
---|---|---|
Invoke |
|
|
For |
|
|
ForEach |
|
|
逐次処理とほとんど同じ書き方で並列処理ができます。
ただし、複数のスレッドから同じデータを読み書きする場合には「排他制御」が必要なので注意してください。 例えば、以下のような処理は、単に 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);
ロック自体がそれなりにオーバーヘッドのかかる処理なので、 この例の場合、並列化するとかえって遅くなる可能性があります。
Parallel LINQ
「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();