ページ

2009年5月28日木曜日

[.NET] Parallel.For ループからとっとと抜ける方法

Exiting from Parallel Loops Early より
普通の for ループでは break と書けば簡単にループから抜け出せます。
んじゃ、Parallel.For や Parallel.ForEach では?とかそういう話。

最初に書いておきますが、いまだに Parallel Extension を使ったこともありませんし、試す環境もないのでブログの記事とかリファレンスとかだけを見た知識で書いてます。
なので、間違ってたらごめんなさい。
というか、間違ってたらぜひ教えてください。

で、上記の記事の前半では例外について書かれてます。
普通のループでは例外が発生しても問題ありませんし、ループの中だろうが外だろうが好きなところで catch できます。
しかし、Parallel ループではそうはいきません。
それぞれのイテレーション (ループの一回分のこと) はどのスレッドで実行されるかは不定ですから例外が外に伝わってくるとは限りませんし、どこかのイテレーションで例外が発生してもループが止まるとは限りません。
んじゃ、どうなるか?
上記の記事によるとどうやら

  • Parallel.For / ForEach 内で例外が出るとランタイムがキャッチする。
  • 例外が出た後は新しいイテレーションを開始しないようにする。
  • ただ、すでに並列に動いているイテレーションは最後まで実行される。
  • 動いている全部のイテレーションが終わったら、発生した例外を全部集めて System.AggregateException にぶち込んで、こいつを throw する。

ということみたいです。
コードで書くと、

try
{
Parallel.For(0, N, i =>
{
throw new Exception("例外");
});
}
catch (AggregateException ex)
{
// ex.InnerExceptions.Count は 2 以上の可能性もある
}

ということだと思います。
あと、一つのイテレーションが長い場合、他のイテレーションで例外が発生したかどうかは ParallelLoopState.IsExceptional で判断できるそうです。
Parallel.For / ForEach には body が Action<int, ParallelLoopState> となっているバージョンもあります。
それを使って以下のように書けるわけです。

Parallel.For(0, N, (i, loopState) =>
{
// いろいろ処理

if (loopState.IsExceptional) { // 他のイテレーションで例外が出てるので終了する return; }
// いろいろ処理 });

続いてループを抜ける件について。
これには ParallelLoopState.Stop() メソッドと Break() メソッドが使えます。
まずは、Stop() メソッドについて。

  • Stop() が呼ばれた後は新しいイテレーションを開始しないようにする。
  • ただ、すでに並列に動いているイテレーションは最後まで実行される。
  • 他のイテレーションが Stop() を呼んだかどうかは ParallelLoopState.IsStopped で判断できる。
  • 動いている全部のイテレーションが終わったら、Parallel.For / ForEach が終了する。
  • Stop() が呼ばれたかどうかは ParallelLoopResult.IsCompleted で判断できる。ちなみに、ParallelLoopResult は Parallel.For / ForEach の戻り値。

ということみたいです。
コードで書くと以下みたいな感じ。

Parallel.For(0, N, (i, loopState) =>
{

if (loopState.IsStopped) { // 他のイテレーションが Stop() を呼んだので終了する return; }

if (終了条件) { // もう終わる loopState.Stop(); return; }
});

続いて Break() です。
これは、http://msdn.microsoft.com/en-us/library/system.threading.parallelloopstate.break(VS.100).aspx を見ると、

  • Break() が呼ばれた後はより大きい数のイテレーションは開始しないようにする。
  • すでに並列に動いているイテレーションは最後まで実行される。
  • 一番小さい数が ParallelLoopState.LowestBreakIteration にセットされる。
  • Break() が呼び出されたかどうかは ParallelLoopState.LowestBreakIteration が null かどうかで判断できる。(ParallelLoopState.LowestBreakIteration は Nullable<long> です)
  • ParallelLoopState.LowestBreakIteration が確定したら、Parallel.For / ForEach が終了する。
  • Break() が呼ばれたかどうかは ParallelLoopResult.LowestBreakIteration で判断できる。

ということみたいです。
ちょっとどう表現したらいいのかアレなんですが、要するに以下みたいなことだと思います。

0~1000 の Parallel.For ループがあったとします。
このうち、100番のイテレーションで Break() を呼んだとします。
すると 101~1000 のイテレーションは実行する必要が無いことが確定します。
また、この時点では ParallelLoopState.LowestBreakIteration に 100 がセットされます。
Parallel ループでは、イテレーションが並列で実行されるだけでなく、どういう順番で実行されるかも不定です。
ですから、0~99 のイテレーションに実行していないものがあるかもしれません。
もしあったらそれらのイテレーションを実行します。
そして、Break() を呼び出すより小さいイテレーションがあったらその数に ParallelLoopState.LowestBreakIteration が更新されます。
こうして Break() を呼び出す最小のイテレーションを求めて、その数が ParallelLoopState.LowestBreakIteration にセットされ Parallel ループが終了します。

どうだろ?
あってるかなぁ?
試す環境が無いのでちょっと自信が無いところもあるんですが、きっとこういうことだと思います。

Parallel Extensions なんてだいぶ前から CTP とか出てたし、最初はこういった解説ぐらいいくらでもあるんだろうと思ってたんですよね。
で、なにげにググってみたら ParallelLoopState だと 36件、ParallelLoopResult だと 6件という、びっくりするような結果で、もちろん日本語のページは一つも無し。
なので試す環境も無いくせに思わず書いてみましたw

あと、Parallel ループの中で気軽に使える ConcurrentQueue、ConcurrentStack なんていうスレッドセーフ版のコレクションも増えてるんですね。
http://msdn.microsoft.com/en-us/library/system.collections.concurrent(VS.100).aspx
これはなにげに便利そうだな。


0 件のコメント:

コメントを投稿