web-dev-qa-db-ja.com

Parallel.For():ループ外の変数を更新します

.NET4.0の新機能を調べているところです。それで、私は_Parallel.For_と通常のfor(x;x;x)ループを使用して簡単な計算を試みています。

ただし、約50%の確率で異なる結果が得られます。

_long sum = 0;

Parallel.For(1, 10000, y =>
    {
        sum += y;
    }
);

Console.WriteLine(sum.ToString());

sum = 0;

for (int y = 1; y < 10000; y++)
{
   sum += y;
}
Console.WriteLine(sum.ToString());
_

私の推測では、スレッドは同時に「合計」を更新しようとしています。
それを回避する明白な方法はありますか?

35
Inisheer

あなたはこれを行うことはできません。 sumは並列スレッド間で共有されています。 sum変数が一度に1つのスレッドによってのみアクセスされていることを確認する必要があります。

// DON'T DO THIS!
Parallel.For(0, data.Count, i =>
{
    Interlocked.Add(ref sum, data[i]);
});

しかし...各スレッドがInterlocked.Addをロックするため、ループを効果的にシリアル化したため、これはアンチパターンです。

あなたがする必要があるのは、小計を追加し、次のように最後にそれらをマージすることです:

Parallel.For<int>(0, result.Count, () => 0, (i, loop, subtotal) =>
    {
        subtotal += result[i];
        return subtotal;
    },
    (x) => Interlocked.Add(ref sum, x)
);

これについての詳細はMSDNで見つけることができます: http://msdn.Microsoft.com/en-us/library/dd460703.aspx

プラグ:これについて詳しくは、第2章 並列プログラミングガイド を参照してください。

以下も間違いなく読む価値があります...

並列プログラミングのパターン:.NET Framework 4を使用した並列パターンの理解と適用-StephenToub

69
Ade Miller

sum += y;は実際にはsum = sum + y;です。次の競合状態が原因で、誤った結果が得られます。

  1. Thread1はsumを読み取ります
  2. Thread2はsumを読み取ります
  3. Thread1はsum+y1を計算し、結果をsumに格納します
  4. Thread2はsum+y2を計算し、結果をsumに格納します

sumは、sum+y2ではなくsum+y1+y2と等しくなりました。

あなたの推測は正しいです。

あなたが書くときsum += y、ランタイムは次のことを行います。

  1. フィールドをスタックに読み込む
  2. スタックにyを追加します
  3. 結果をフィールドに書き戻します

2つのスレッドが同時にフィールドを読み取る場合、最初のスレッドによって行われた変更は2番目のスレッドによって上書きされます。

使用する必要があります Interlocked.Add 、単一のアトミック操作として加算を実行します。

5
SLaks

上で述べたように、ループの各反復は前に依存しているため、このループは並列処理のために分割できないことを区別することが重要だと思います。 parallel forは、ピクセルスケーリングなどの明示的に並列なタスク用に設計されています。これは、ループの各反復が、その反復以外のデータ依存関係を持つことができないためです。

Parallel.For(0, input.length, x =>
{
    output[x] = input[x] * scalingFactor;
});

上記の並列処理のための簡単なパーティション分割を可能にするコードの例。ただし、警告の言葉として、並列処理にはコストが伴います。上記の例で使用したループでさえ、並列処理によって節約された時間よりもセットアップ時間が長くなるため、並列処理を気にするのは非常に単純すぎます。

4
Mgetz

Longをインクリメントすることは、アトミック操作ではありません。

4
Eric Mickelsen

誰も言及していない重要な点:データ並列操作(OPなど)の場合、Parallelクラスの代わりにPLINQを使用する方が(効率と単純さの両方の観点から)多くの場合優れています。 OPのコードは、実際には並列化するのは簡単です。

long sum = Enumerable.Range(1, 10000).AsParallel().Sum();

上記のスニペットは ParallelEnumerable.Sum メソッド。ただし、より一般的なシナリオでは Aggregate を使用することもできます。これらのアプローチの説明については、 並列ループ の章を参照してください。

3
Douglas