我有一个foreach
循环,我正在并行化,我注意到一些奇怪的东西。代码看起来像
double sum = 0.0;
Parallel.ForEach(myCollection, arg =>
{
sum += ComplicatedFunction(arg);
});
// Use sum variable below
当我使用常规foreach
循环时,我会得到不同的结果。 ComplicatedFunction
内部可能存在更深层次的内容,但sum
变量可能会受到并行化的意外影响吗?
答案 0 :(得分:30)
并行化可能会对sum变量产生意外影响吗?
是。
对double
的访问不是原子的,sum += ...
操作永远不是线程安全的,即使对于原子类型也是如此。所以你有多种竞争条件,结果是不可预测的。
您可以使用以下内容:
double sum = myCollection.AsParallel().Sum(arg => ComplicatedFunction(arg));
或者,用更短的符号
double sum = myCollection.AsParallel().Sum(ComplicatedFunction);
答案 1 :(得分:11)
与上面提到的其他答案一样,从多个线程(这是Parallel.ForEach所做的)更新sum
变量不是线程安全的操作。在进行更新之前获取锁定的简单修复将修复 问题。
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;
}
});
答案 2 :(得分:4)
如果你认为sum += ComplicatedFunction
实际上由一堆操作组成,请说:
r1 <- Load current value of sum
r2 <- ComplicatedFunction(...)
r1 <- r1 + r2
所以现在我们随机交错两个(或更多)并行实例。一个线程可能持有用于执行其计算的总和的陈旧“旧值”,其结果是在一些修改版本的sum之上写回。这是一种经典的竞争条件,因为根据交错的方式,某些结果会以不确定的方式丢失。
答案 3 :(得分:-1)
或者您可以使用.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.
);