最初看似简单解决方案的问题已被证明是一个非常有趣的挑战。
我有一个类维护一个内部固定大小、线程安全的集合(通过对所有插入和删除操作使用 lock
)并通过其属性提供各种统计值。
一个例子:
public double StandardDeviation {
get {
return Math.Sqrt((Sum2 - ((Sum * Sum) / Count)) / Count);
}
}
现在,我已经彻底测试了这个计算,通过集合运行 10,000 个值并检查每次更新的标准偏差。它工作正常...在单线程场景中。
然而,在我们的开发和生产环境的多线程上下文中出现了一个问题。似乎这个数字以某种方式有时返回NaN
,然后迅速变回实数。当然,这一定是由于传递给 Math.Sqrt
的负值所致。我只能想象,当计算进行到一半时,计算中使用的值之一由单独的线程更新时会发生这种情况。
我可以先缓存值:
int C = this.Count;
double S = this.Sum;
double S2 = this.Sum2;
return Math.Sqrt((S2 - (S * S) / C) / C);
但是 Sum2
可能仍会更新,例如,在设置 S = this.Sum
之后,再次影响计算。
我可以在代码中更新这些值的所有点周围放置一个锁
:
protected void ItemAdded(double item) {
// ...
lock (this.CalculationLock) {
this.Sum += item;
this.Sum2 += (item * item);
}
}
然后,如果我在计算 StandardDeviation
时锁定
同一对象,我认为最终会解决问题。它没有。该值仍然以 NaN
的形式出现,但并不频繁。
坦率地说,即使上述解决方案已经 奏效,它也非常困惑并且对我来说似乎不太好管理。 是否有标准和/或更直接的方法来实现计算值的线程安全,例如这样?
编辑:原来这里有一个问题示例,起初似乎只有一个可能的解释,但毕竟问题完全出在其他问题上。
我一直一丝不苟地以各种可能的方式实现线程安全,而尽可能不牺牲巨大的性能——锁定对共享值的读取和写入(例如,Sum
和 Count
),在本地缓存值,并使用相同的锁对象来修改集合和更新共享值……老实说,这一切看起来都有些矫枉过正。
没有任何效果;那个邪恶的 NaN
不断弹出。所以我决定在 StandardDeviation
返回 NaN
时将集合中的所有值打印到控制台...
我立即注意到,当集合中的所有值都相同时,似乎总是会发生这种情况。
这是官方的:我被浮点运算烧坏了。 (所有的值都相同,所以 StandardDeviation
中的被除数——即被取平方根的数——被计算为某个极小的负数。)
最佳答案
I could put a lock around all points in the code where these values are updated:
protected void ItemAdded(double item) {
// ...
lock (this.CalculationLock) {
this.Sum += item;
this.Sum2 += (item * item);
}
}
Then if I lock on this same object when calculating StandardDeviation, I thought that would, finally, fix the problem. It didn't. The value is still coming in as NaN on a fleeting, infrequent basis.
这正是您应该为正确性做的事情。如果这对您不起作用,我建议您要么错过了更新场景 - 或者您有其他问题(例如 Sum 或 Sum2 偶尔为 NaN
或由于某些 而出现意外值其他竞争条件)。
关于c# - 使计算线程安全的标准方法是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1544142/