Parallel.ForEach 的不同求和结果
Posted
技术标签:
【中文标题】Parallel.ForEach 的不同求和结果【英文标题】:Different summation results with Parallel.ForEach 【发布时间】:2010-07-29 21:58:33 【问题描述】:我有一个正在并行化的foreach
循环,我注意到一些奇怪的东西。代码看起来像
double sum = 0.0;
Parallel.ForEach(myCollection, arg =>
sum += ComplicatedFunction(arg);
);
// Use sum variable below
当我使用常规的 foreach
循环时,我会得到不同的结果。 ComplicatedFunction
内部可能有更深层次的东西,但 sum
变量可能会意外地受到并行化的影响?
【问题讨论】:
参见 [在 parallel.foreach 范围外增加一个计数值](***.com/questions/2394447/…)。基本上,如果需要,您可以使用Interlocked,但最好尽可能避免副作用。 【参考方案1】:sum 变量是否可能意外地受到并行化的影响?
是的。
对double
的访问不是原子的,sum += ...
操作永远不是线程安全的,即使对于原子类型也是如此。所以你有多个竞争条件,结果是不可预测的。
你可以使用类似的东西:
double sum = myCollection.AsParallel().Sum(arg => ComplicatedFunction(arg));
或者,简而言之
double sum = myCollection.AsParallel().Sum(ComplicatedFunction);
【讨论】:
根据原帖,myCollection
是否需要线程安全?
@Kevin - 不,任何IEnumerable<T>
都可以。
是因为Parallel.ForEach
知道如何以安全的方式处理线程不安全的集合吗? (另外,为了清楚起见,原文我指的是 Brian Triplett 的原始问题,而不是您的解决方案)。
是的,.ForEach() 和 .Asparallel() 都设计用于普通集合。
“变量int sum
本来是原子的” - 这在哪里可以保证是真的?【参考方案2】:
与提到的其他答案一样,从多个线程更新 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;
);
【讨论】:
你为什么要鼓励改造一个非常标准的***? @Novelocrat:对不起,我不清楚你在问什么。另外,由于时间原因,我可以假设您对这个答案投了反对票吗?如果是这样,答案的哪一部分是错误的?我仔细检查了代码语法,分区策略是进行Parallel.For
操作的一种非常成熟的做法,但也许我错过了一些引起你注意的东西。
您描述的实现正是 Henk 的答案描述的库函数。此外,我强烈怀疑在库实现中减少每个线程的小计(“累加器”)比基于锁的方法更有效。
是的,我也更喜欢 Henk 的回答。事实上,我是赞成票之一。只是好奇......这个答案中的什么使它更有害而不是有用,我可以改变什么来使它更有帮助?请记住,我想保留我的初衷,即演示如何以更一般的方式处理并行化。否则,根据 Henk 的帖子保留答案没有多大意义。
哦,是的,使用 PLinq 求和操作更有效地累积分区,因为不使用任何锁或线程同步原语。这部分操作同步发生在调用者线程上。不幸的是,Parallel.For
不支持相同的习语。使用Parallel.For
方法的无锁累积模式会稍微复杂一些,当然也更难解释,因为它需要在while 循环中调用Interlocked.CompareExchange
。【参考方案3】:
如果你认为sum += ComplicatedFunction
实际上是由一堆操作组成的,那么说:
r1 <- Load current value of sum
r2 <- ComplicatedFunction(...)
r1 <- r1 + r2
所以现在我们随机交错两个(或更多)并行实例。一个线程可能持有一个陈旧的“旧值” sum 用于执行其计算,其结果在 sum 的某些修改版本之上写回。这是一个典型的竞争条件,因为基于交错的完成方式,一些结果会以一种不确定的方式丢失。
【讨论】:
你说得对,但实际上情况比你说的要糟糕得多。不仅仅是加载、计算和存储操作不是原子的。甚至不能保证访问双精度中的 位 是原子的! C# 规范仅保证访问 32 位(和更小)数字类型和引用是原子的。双打是 64 位的,因此不能保证是原子的。该程序可以实现为: r1 一半已复制。 说得好。我想在示例中为简单起见,我只是假设基本操作的原子性,但显然正如您所指出的,最坏的情况更加可怕。【参考方案4】:或者您可以使用.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.
);
【讨论】:
以上是关于Parallel.ForEach 的不同求和结果的主要内容,如果未能解决你的问题,请参考以下文章