2010-07-29 6 views
17

나는 병렬 처리하는 foreach 루프가 있으며 이상한 것으로 나타났습니다. 내가 다른 결과를 얻을 정기적 foreach 루프를 사용하는 경우 코드는Parallel.ForEach를 사용하여 다른 합계 결과

double sum = 0.0; 

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

// Use sum variable below 

것 같습니다. ComplicatedFunction 안에 더 깊은 무언가가있을 수 있지만 sum 변수가 병렬화의 영향을받지 않을 가능성이 있습니까?

+1

보기 [ 증가 parallel.foreach 범위 외부 카운트 값 (http://stackoverflow.com/questions/2394447/increment-a-count-value-outside-parallel-foreach-scope). 기본적으로 [Interlocked] (http://msdn.microsoft.com/en-us/library/55dzx06b.aspx)를 사용할 수 있지만 가능한 경우 부작용을 피하는 것이 좋습니다. –

답변

28

병렬 변수가 합계 변수에 영향을 줄 가능성이 있습니까?

예.
double에 대한 액세스는 원 자성이 아니므로 sum += ... 작업은 원자 적 유형의 경우조차도 스레드로부터 안전하지 않습니다. 따라서 여러 경쟁 조건이 있으며 결과는 예측할 수 없습니다.

당신은 같은 것을 사용할 수 있습니다 : 당신이 실제로 작업의 무리로 구성되는 것으로 그 sum += ComplicatedFunction에 대해 생각한다면 짧은 표기

double sum = myCollection.AsParallel().Sum(ComplicatedFunction); 
+0

이것은 효과가 있습니다. LINQ를 사용하여 깨끗한 구현. –

+0

원래 게시물마다 'myCollection'이 스레드로부터 안전해야합니까? –

+0

@Kevin - 아니요, 'IEnumerable '이면됩니다. –

4

에,

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

을하거나, 말 :

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

이제 우리는 두 개 (또는 그 이상)의 병렬 인스턴스를 무작위로 인터리브합니다. 하나의 스레드가 계산을 수행하는 데 사용하는 오래된 "오래된 값"의 합계를 보유하고있을 수 있으며 결과가 일부 수정 된 버전의 합계 위에 다시 쓰여집니다. 그것은 고전적인 경쟁 조건입니다. 왜냐하면 일부 결과는 인터리빙이 수행되는 방식에 따라 비 결정적 방식으로 손실되기 때문입니다.

+5

당신 말이 맞습니다. 그러나 상황은 사실 당신이 말하는 것보다 훨씬 더 나쁩니다. 로드, 계산 및 저장 작업이 원자 적이지 않은 것은 아닙니다. 더블에서 * 비트 *에 액세스하는 것은 원자가되는 것을 보장하지 않습니다! C# 사양은 32 비트 (및보다 작은) 숫자 형식 및 참조를 액세스하는 것이 절대적이라는 것을 보증합니다. 복식은 64 비트이기 때문에 원자적일 수 있습니다. 이 프로그램은 다음과 같이 구현 될 수 있습니다 : r1 <- 합계의 최상위 32 비트, r1 <-로드의 최하위 32 비트 ... 두 배의 * 절반이 복사되는 동안 연산이 인터리브 될 수 있음을 의미합니다. –

+0

잘 설명되어 있습니다. 이 예제에서 단순함을 알기 위해 기본 작업의 원 자성을 가정했습니다. 그러나 분명히 지적했듯이 최악의 경우는 훨씬 더 끔찍합니다. – Gian

11

다른 답변과 마찬가지로, 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; 
     } 
    }); 
+1

왜 표준 바퀴의 재창조를 장려하고 있습니까? – Novelocrat

+0

@Novelocrat : 미안하지만, 당신이 무엇을 요구하고 있는지 명확하지 않습니다. 또한, 타이밍 때문에 나는 당신이 대답을 downvoted 가정 수 있습니까? 그렇다면 대답의 어떤 부분이 잘못 되었습니까? 나는 코드 구문을 두 번 확인했고 파티셔닝 전략은'Parallel.For' 연산을 수행하는 꽤 잘 정립 된 방법이지만 어쩌면 내가 눈을 뜬 것을 놓친 것일 수도있다. –

+0

당신이 구현하는 것을 설명하는 것은 Henk의 답이 묘사하는 라이브러리 함수와 정확히 같습니다. 또한 라이브러리 구현에서 각 스레드의 소계 ('누산기')가 잠금 기반 접근 방식보다 훨씬 효율적이라고 생각합니다. – Novelocrat

관련 문제