Подтвердить что ты не робот

Параллель .For(): обновить переменную вне цикла

Я просто заглядываю в новые возможности .NET 4.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());

Я предполагаю, что потоки пытаются обновить "сумму" одновременно.
Есть ли очевидный путь вокруг него?

4b9b3361

Ответ 1

Вы не можете этого сделать. sum используется совместно с вами параллельными потоками. Вы должны убедиться, что переменная sum доступна только одному потоку за раз:

// 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

PLUG: Подробнее об этом вы можете найти в главе 2 на Руководство по параллельному программированию

Следующее также обязательно стоит прочитать...

Шаблоны для параллельного программирования: понимание и применение параллельных шаблонов с .NET Framework 4 - Stephen Toub

Ответ 2

sum += y; на самом деле sum = sum + y;. Вы получаете неправильные результаты из-за следующего состояния гонки:

  • Thread1 читает sum
  • Thread2 читает sum
  • Thread1 вычисляет sum+y1 и сохраняет результат в sum
  • Thread2 вычисляет sum+y2 и сохраняет результат в sum

sum теперь равен sum+y2 вместо sum+y1+y2.

Ответ 3

Ваше предположение верно.

Когда вы пишете sum += y, среда выполнения делает следующее:

  • Прочитайте поле в стеке
  • Добавить y в стек
  • Запишите результат обратно в поле

Если два потока одновременно прочитают поле, изменение, сделанное первым потоком, будет перезаписано вторым потоком.

Вам нужно использовать Interlocked.Add, который выполняет добавление как одну атомную операцию.

Ответ 4

Увеличение длины - это не атомная операция.

Ответ 5

Я считаю важным отметить, что этот цикл не может быть разбит на разделы для parallelism, потому что, как уже упоминалось выше, каждая итерация цикла зависит от предыдущего. Параметр for предназначен для явно параллельных задач, таких как масштабирование пикселей и т.д., Потому что каждая итерация цикла не может иметь зависимостей данных за пределами его итерации.

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

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

Ответ 6

Важный момент, который, как ни странно, не упоминал: для параллельных операций с данными (таких как OP) часто лучше (с точки зрения эффективности и простоты) использовать PLINQ вместо класса Parallel. Код OP фактически тривиален для распараллеливания:

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

В приведенном выше фрагменте используется метод ParallelEnumerable.Sum, хотя можно также использовать Aggregate для более общих сценариев. Для объяснения этих подходов обратитесь к главе Parallel Loops.

Ответ 7

если в этом коде есть два параметра. Например

long sum1 = 0;
long sum2 = 0;

Parallel.For(1, 10000, y =>
    {
        sum1 += y;
        sum2=sum1*y;
    }
);

что мы будем делать? Я предполагаю, что нужно использовать массив!