2010-05-05 2 views
35

Я просто ищу новые функции .NET 4.0. При этом я пытаюсь выполнить простой расчет, используя Parallel.For и обычный цикл for(x;x;x).Параллель.For(): Обновление переменной за пределами цикла

Тем не менее, я получаю разные результаты примерно в 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()); 

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

+4

Параллельное программирование выполняется в двух частях: 1) выполняется в отдельном потоке и 2) синхронизирует \ передает по потокам. параллельные расширения позволяют 1), однако 2) должны быть явно адресованы разработчиком, а когда вы говорите 'sum + = y;', вы эффективно используете каждый поток, добавляя меня к сумме! все вместе. вам нужно синхронизировать их общение в отношении общего ресурса 'sum' –

+24

мои страхи сбываются ... параллельные расширения позволяют людям писать параллельный код без понимания теории, в том числе условий гонки (как здесь) – Andrey

+25

@ Andrey - Да, поэтому некоторые из нас на самом деле пытаются сначала изучить его (т. е. опубликовать вопросы о SO). – Inisheer

ответ

68

Вы не можете этого сделать. 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

ВИЛКИ: Вы можете узнать больше об этом в главе 2 на A Guide to Parallel Programming

Ниже также определенно стоит прочитать ...

Patterns for Parallel Programming: Understanding and Applying Parallel Patterns with the .NET Framework 4 - Stephen Toub

+0

Удивительный ответ! – Andrey

+1

Где я могу найти точное объяснение перегрузки, которую вы использовали в этом ответе? –

+0

@Alex. Дополнительную информацию можно найти здесь: http://msdn.microsoft.com/en-us/library/dd460703.aspx. Я обновил ответ по той же ссылке. –

4

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

+0

Хорошая точка, SLaks. @TSS: здесь есть две операции, добавление и сохранение значения - вам действительно нужно блокировать. –

5

Ваша догадка верна.

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

  1. Прочитайте поле на стек
  2. Добавить y в стек
  3. Записать результат обратно в поле

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

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

+5

См. Ниже. Неверный способ использования блокировки добавить просто сериализовать ваш цикл. –

+0

Я бы добавил, что лучший способ использовать локальную переменную и после цикла добавить их в один глобальный, используя Interlocked.Add, конечно. – Andrey

+1

Вот пример этого в моем ответе ниже. –

16

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

  1. Резьба1 считываемых sum
  2. Резьба2 читает sum
  3. Резьба1 вычисляет sum+y1, и сохраняет результат в sum
  4. thread2 вычисляет sum+y2, и сохраняет результат sum

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

+2

+1 для объяснения состояния гонки. –

4

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

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

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

+0

Вы можете разделить его на параллелизм, вам просто нужно подумать об этом по-другому, агрегации. –

+0

...MPI_AllGather() будет хорошим примером, однако некоторые поверхностные исследования MSDN показывают, что вам нужно будет обратиться к MPI #, чтобы получить эту функциональность ... поскольку она, похоже, не включена. Вы могли бы, однако, написать свой собственный. – Mgetz

-1

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

long sum1 = 0; 
long sum2 = 0; 

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

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

3

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

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

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

Смежные вопросы