背景
並列計算を行う上でとても便利なParallelですが、いざプログラムを実行していて注意しなければならないことが見つかったので備忘録として残しておきます。
static void Main(string[] args)
{
Debug.WriteLine("start :" + DateTime.Now);
Parallel.For(0, 10, (i) =>
{
Thread.Sleep(2000); // 重い処理
});
Debug.WriteLine("finished :" + DateTime.Now);
}
上のコードが正しいParallelの使い方で、実行すると実行環境にもよりますが、
start :2015/04/16 18:21:01
finished :2015/04/16 18:21:05 // 順番に実行すると10secかかるところを4secで完了
となります。しかし、GUIプログラミングをしているときに、画面が固まらないように新しいTaskを作成してバックグラウンド処理を行うプログラムを書いていると、以下のようなコードを書いてしまうことがあります。(僕だけかもしれませんが)
static void Main(string[] args)
{
Debug.WriteLine("start :" + DateTime.Now);
Parallel.For(0, 10, (i) =>
{
backGroundTask(i);
});
Debug.WriteLine("finished :" + DateTime.Now);
}
static void backGroundTask(int i)
{
Debug.WriteLine("backGround start :" + i + " " + DateTime.Now);
Task.Factory.StartNew(() =>
{
Thread.Sleep(2000); // 重い処理
Debug.WriteLine("backGround finished:" + i + " " + DateTime.Now);
});
}
これを実行すると以下のような出力になると思います。(iはParallelによる並列処理のため、実行時によって異なります)
start :2015/04/16 18:29:17
backGround start :0 2015/04/16 18:29:17
backGround start :1 2015/04/16 18:29:17
backGround start :3 2015/04/16 18:29:17
backGround start :2 2015/04/16 18:29:17
backGround start :4 2015/04/16 18:29:17
backGround start :6 2015/04/16 18:29:17
backGround start :8 2015/04/16 18:29:17
backGround start :7 2015/04/16 18:29:17
backGround start :9 2015/04/16 18:29:17
backGround start :5 2015/04/16 18:29:17
finished :2015/04/16 18:29:17 // 一瞬で完了している
backGround finished が呼び出されていません。
考えてみれば当たり前で、Parallelで実行するメソッド内で新しくTaskが作成され、その中の重い処理は別のスレッドで実行されます。つまり、Parallelが実行するメソッドは Taskを作成する処理のみを行う ため、すぐに完了してしまうのです。
これは以下のコードで 一応 解決出来ます。しかし、以下のように書くならそもそもTaskを作るべきではないですし、コードが汚くなるので個人的には嫌いです。
static void Main(string[] args)
{
Debug.WriteLine("start :" + DateTime.Now);
Parallel.For(0, 10, (i) =>
{
backGroundTask(i).Wait(); // Taskの完了を待つ
});
Debug.WriteLine("finished :" + DateTime.Now);
}
// Taskを返すメソッド(C#4.5以下の書き方)
static Task backGroundTask(int i)
{
Debug.WriteLine("backGround start :" + i + " " + DateTime.Now);
return Task.Factory.StartNew(() =>
{
Thread.Sleep(2000); // 重い処理
Debug.WriteLine("backGround finished:" + i + " " + DateTime.Now);
});
}
実行結果は長いですが以下のようになります。ちゃんと全てのTaskが終了しているのがわかりますね。
start :2015/04/16 18:39:23
backGround start :0 2015/04/16 18:39:23
backGround start :2 2015/04/16 18:39:23
backGround start :4 2015/04/16 18:39:23
backGround start :6 2015/04/16 18:39:23
backGround start :8 2015/04/16 18:39:24
backGround start :1 2015/04/16 18:39:25
backGround finished:0 2015/04/16 18:39:25
backGround finished:4 2015/04/16 18:39:25
backGround finished:2 2015/04/16 18:39:25
backGround finished:6 2015/04/16 18:39:25
backGround start :3 2015/04/16 18:39:25
backGround start :7 2015/04/16 18:39:25
backGround start :5 2015/04/16 18:39:25
backGround start :9 2015/04/16 18:39:25
backGround finished:8 2015/04/16 18:39:26
backGround finished:1 2015/04/16 18:39:27
backGround finished:3 2015/04/16 18:39:27
backGround finished:7 2015/04/16 18:39:27
backGround finished:5 2015/04/16 18:39:27
backGround finished:9 2015/04/16 18:39:27
finished :2015/04/16 18:39:27 // 順番に実行すると10secかかるところを4secで完了
足し算がおかしい現象
僕が実際に犯してしまった間違いは以下のようなコードでした。
static void Main(string[] args)
{
Debug.WriteLine("start :" + DateTime.Now);
var sum = 0;
Parallel.For(0, 10, (i) =>
{
Thread.Sleep(3000); // 重い処理 1
backGroundTask(i).ContinueWith(t => sum += i);
});
Debug.WriteLine("finished :" + DateTime.Now);
Debug.WriteLine("sum :" + sum);
}
static Task<int> backGroundTask(int i)
{
Debug.WriteLine("backGround start :" + i + " " + DateTime.Now);
return Task<int>.Factory.StartNew(() =>
{
Thread.Sleep(2000); // 重い処理 2
Debug.WriteLine("backGround finished:" + i + " " + DateTime.Now);
return i;
});
}
Parallel内にも重い処理があるため、先に立ち上がったbackGroundTaskは終了しますが、全てのbackGroundTaskが終了する前にParallelループが終了してしまいます。そのため、中途半端な結果が出力されます。
start :2015/04/16 18:51:21
backGround start :0 2015/04/16 18:51:24
backGround start :2 2015/04/16 18:51:24
backGround start :4 2015/04/16 18:51:24
backGround start :6 2015/04/16 18:51:24
backGround start :8 2015/04/16 18:51:24
backGround start :1 2015/04/16 18:51:25
backGround start :3 2015/04/16 18:51:25
backGround finished:4 2015/04/16 18:51:26
backGround finished:6 2015/04/16 18:51:26
backGround finished:8 2015/04/16 18:51:26
backGround start :5 2015/04/16 18:51:26
backGround start :7 2015/04/16 18:51:27
backGround finished:0 2015/04/16 18:51:27
backGround finished:1 2015/04/16 18:51:27
backGround start :9 2015/04/16 18:51:27
finished :2015/04/16 18:51:27
sum :19 // 45を期待していたのに、数値が異なる
このプログラムの最大の問題は、__実行時によって結果が異なる__ということです。重い処理1がThread.Sleep(3000)と固定値になっているため、まだましですが、これが例えば Thread.Sleep(10000 * new Random().NextDouble()) だとしたら、__さっき実行したときはうまく動いていたのに、何か動かなくなった__ということが起こります(経験談)。
こういうミスはコンパイルエラーを出してくれれば助かるのですが。