Другое суммирование заканчивается с Параллелью. ForEach

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

double sum = 0.0;

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

// Use sum variable below

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

18
задан Henk Holterman 29 July 2010 в 22:15
поделиться

3 ответа

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

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

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

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

или, в более коротком обозначении

double sum = myCollection.AsParallel().Sum(ComplicatedFunction);
30
ответ дан 30 November 2019 в 06:50
поделиться

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

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

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

4
ответ дан 30 November 2019 в 06:50
поделиться

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

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

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

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

Вот как вы это делаете.

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;
        }
    });
11
ответ дан 30 November 2019 в 06:50
поделиться
Другие вопросы по тегам:

Похожие вопросы: