|
|
連載:C# 4入門
第3回 TaskクラスとPLINQ(Parallel LINQ)
株式会社ピーデー 川俣 晶
2010/09/17 |
|
|
PLINQのワナ
PLINQはクエリを並列実行可能にしてくれる。そのための手順はAsParallelメソッドの挿入だけである。
問題は、並列実行そのものにある。つまり、適切な同期が設計されていないとデータが狂う可能性が出てくるのである。また、並列実行されると順番が保存されない。並列に実行するということは、いくつかのデータをいっせいに処理するわけで、どのデータの処理が最初に終わるか明確ではない。だから、例えば以下のようなプログラムは、クエリ式にAsParallelメソッドを入れるだけで結果が変わってしまう可能性がある。
using System;
using System.Linq;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
int[] ar = { 1, 2, 3 };
var q1 = from n in ar select n;
int sum = 1;
foreach (var n in q1)
{
Console.WriteLine("sum={0}×{1}+{1}", sum, n);
sum = sum * n + n;
}
Console.WriteLine("result {0}", sum);
}
}
|
|
リスト5 |
sum=1×1+1
sum=2×2+2
sum=6×3+3
result 21
|
|
リスト5の実行結果 |
このプログラムのクエリ式に、以下のようにAsParallelメソッドを入れてみよう。
var q1 = from n in ar.AsParallel() select n;
|
|
sum=1×1+1
sum=2×3+3
sum=9×2+2
result 20
|
|
リスト5の実行結果 |
結果はシステムなどにより不定であり、これは結果の一例。 |
この場合、幸いにして計算そのものは狂っていないが、順番が狂っているため、最終的な結果が違ってしまう。間違いなく出力した値と計算内容が同じであることを保証するには、以下のようにロック専用オブジェクトと、lockステートメントなどを追加するとよいだろう。
object o = new object();
foreach (var n in q1)
{
lock (o)
{
Console.WriteLine("sum={0}×{1}+{1}", sum, n);
sum = sum * n + n;
}
}
|
|
順番の方は、以下のように順番を維持することを指定するAsOrderedメソッドをメソッド・チェーンに追加して解決する。
var q1 = from n in ar.AsParallel().AsOrdered() select n;
|
|
ただし、順番を維持するということは、その分だけ速度が遅くなることを意味する。最大性能を発揮させるには、順番に関係のないクエリを行う必要がある。
ちなみに、「object o = new object();」というコードは人によっては奇異に見えるかもしれない。object型をインスタンス化しても何ら機能を持たないからである。lockステートメントは、型オブジェクトなどを使用すれば、このようなコードは排除できるケースもあると思うが、別の目的のオブジェクトを同期用に転用すると、どこに落とし穴があるか分からない。同期は同期だけで専用のobject型のオブジェクトを作成する方がよいだろう。
このように、同期と順番の問題に絡まるとPLINQはトラブルが起きやすい。しかし、そうではないクエリ式であれば、複数コアによる性能向上というメリットをすぐ甘受できるだろう。
まとめ
今回のまとめ。
- スレッドとタスク(Taskクラス)は似て非なる概念。Taskクラスの方が簡単で軽い
- タスクは終了したときに実行する機能を予約できる
- 複数タスクの終了も簡単に待てる
- PLINQはAsParallelメソッドを追加するだけ
- PLINQには同期の問題があり、順番が維持されない可能性がある
- 順番を維持したい場合はAsOrderedメソッドを追加できるが性能が落ちる
ここでもう1回繰り返そう。
タスクの終了は待ってくれるが時代は待ってくれない。つまり、並列プログラミングがこれから回避できない問題として横たわってくる。だから、ここで問われていることは並列処理を採用するか否かではない。そうではなく、並列処理としてどのような技術を取り込み、どのようなソース・コードを書くのか、ということだ。使用する技術はスレッドやタスク、場合によっては非同期I/Oであることもあるだろう。しかし、何を使うにせよ、並列処理は不可避なのだ。
ちなみに、筆者はマルチCPUの世界をかなり前から狙っていた。Pentium時代には実現できなかったが、Pentium IIIになってようやく実現できた。その後、余りパーツで構築したPentium ProのサーバもマルチCPUになった。それからPentium 4時代になって、手軽にマルチCPUのシステムが手に入らず悲しい思いをしたが、Pentium Dではマルチ・コアになって、CPUが1つでもコアは2つになった。その後はもう、世の中が複数コアCPUだらけである。ノート用の低消費電力のCPUですら、2コアは当たり前である。
しかし、筆者もこのような状況が到来するとは思ってもいなかった。かつて、マルチCPUを求めた理由は、裏で重い処理が走っている状態で別の作業をするケースが多かったからだ。その場合のシステムの快適さは、CPU1個と2個では段違いだったのだ。
だが、いまはもう違う。いま要求されているのは、そういう問題ではない。別のプログラム間の競合を減らすことではなく、1つのプログラムの性能を最大限に発揮させることなのだ。そのためにはコーディングにも新発想が要求される。若者は「お前ら若いくせに頭が固いぞ」といわれないために、それなりの年齢なら「だからもう頭が固いんだ」といわれないために、頭を柔軟に切り替えていこう。永遠に同じ技術でやっていけると錯覚したロートルはプロ/アマを問わず珍しくもないが、ユーザーは漠然とコアが増えれば速くなると期待しているのである。その期待にこたえていこうではないか。
Insider.NET 記事ランキング
本日
月間