Различные результаты суммирования с Parallel.ForEach - PullRequest
18 голосов
/ 30 июля 2010

У меня есть цикл foreach, который я распараллеливаю, и я заметил кое-что странное.Код выглядит как

double sum = 0.0;

Parallel.ForEach(myCollection, arg =>
{
     sum += ComplicatedFunction(arg);
});

// Use sum variable below

Когда я использую обычный цикл foreach, я получаю разные результаты.Внутри ComplicatedFunction может быть что-то более глубокое, но возможно, что на переменную sum неожиданно влияет распараллеливание?

Ответы [ 4 ]

30 голосов
/ 30 июля 2010

возможно, что на переменную суммы неожиданно повлияет распараллеливание?

Да.
Доступ к double не является атомарным, а операция sum += ... никогда не является поточно-ориентированной, даже для атомарных типов. Таким образом, у вас есть несколько условий гонки, и результат непредсказуем.

Вы можете использовать что-то вроде:

double sum = myCollection.AsParallel().Sum(arg => ComplicatedFunction(arg));

или, в более короткой записи

double sum = myCollection.AsParallel().Sum(ComplicatedFunction);
11 голосов
/ 30 июля 2010

Как и другие упомянутые ответы, обновление переменной sum из нескольких потоков (что и делает Parallel.ForEach) не является потокобезопасной операцией.Тривиальное исправление получения блокировки перед обновлением исправит эту проблему .

double sum = 0.0;
Parallel.ForEach(myCollection, arg => 
{ 
  lock (myCollection)
  {
    sum += ComplicatedFunction(arg);
  }
});

Однако это создает еще одну проблему.Поскольку блокировка получается на каждой итерации, это означает, что выполнение каждой итерации будет эффективно сериализовано.Другими словами, было бы лучше просто использовать простой старый цикл * 1007. *.

Теперь, хитрость в получении этого права состоит в том, чтобы разделить проблему на отдельные и независимые блоки.К счастью, это очень легко сделать, когда все, что вы хотите сделать, это суммировать результат итераций, потому что операция суммирования коммутативна и ассоциативна, а промежуточные результаты итераций независимы.

Итак, вот как высделай это.

double sum = 0.0;
Parallel.ForEach(myCollection,
    () => // Initializer
    {
        return 0D;
    },
    (item, state, subtotal) => // Loop body
    {
        return subtotal += ComplicatedFunction(item);
    },
    (subtotal) => // Accumulator
    {
        lock (myCollection)
        {
          sum += subtotal;
        }
    });
4 голосов
/ 30 июля 2010

Если вы думаете, что sum += ComplicatedFunction действительно состоит из нескольких операций, скажите:

r1 <- Load current value of sum
r2 <- ComplicatedFunction(...)
r1 <- r1 + r2

Итак, теперь мы случайно чередуем два (или более) параллельных экземпляра этого. Один поток может содержать устаревшее «старое значение» суммы, которую он использует для выполнения своих вычислений, в результате чего он записывает поверх некоторой модифицированной версии суммы. Это классическое состояние гонки, потому что некоторые результаты теряются недетерминированным образом в зависимости от того, как выполняется чередование.

0 голосов
/ 24 января 2019

Или вы можете использовать параллельные операции агрегации, как правильно определено в .Net . Вот код

        object locker = new object();
        double sum= 0.0;
        Parallel.ForEach(mArray,
                        () => 0.0,                 // Initialize the local value.
                        (i, state, localResult) => localResult + ComplicatedFunction(i), localTotal =>   // Body delegate which returns the new local total.                                                                                                                                           // Add the local value
                            {
                                lock (locker) sum4+= localTotal;
                            }    // to the master value.
                        );
...