【发布时间】:2009-10-09 14:31:26
【问题描述】:
最初看似简单的解决方案的问题已被证明是一个非常有趣的挑战。
我有一个类维护一个内部固定大小、线程安全的集合(通过在所有插入和删除操作上使用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 之后,再次影响计算。
我可以在代码中更新这些值的所有点周围放置一个lock:
protected void ItemAdded(double item) {
// ...
lock (this.CalculationLock) {
this.Sum += item;
this.Sum2 += (item * item);
}
}
如果我在计算StandardDeviation 时在同一对象上lock,我认为最终会解决问题。它没有。该值仍以 NaN 的形式出现,只是转瞬即逝,很少发生。
坦率地说,即使上述解决方案 奏效了,它也很混乱,对我来说似乎不太容易管理。 是否有标准和/或更直接的方法可以在计算值中实现线程安全?
编辑:原来这里有一个问题示例,起初似乎只有一种可能的解释,但毕竟问题完全出在其他问题上。
我一直一丝不苟地以各种可能的方式实现线程安全,并且尽可能不牺牲巨大的性能——锁定对共享值(例如,Sum 和 Count)的读取和写入,缓存值在本地,并使用相同的锁对象来修改集合和更新共享值......老实说,这一切似乎都是多余的。
没有任何效果;那个邪恶的NaN 不断出现。所以我决定在StandardDeviation 返回NaN 时将集合中的所有值打印到控制台...
我立刻注意到,当集合中的所有值都相同时,它似乎总是发生。
这是官方的:我被浮点运算烧坏了。 (所有的值都是相同的,所以StandardDeviation 中的radicand - 即被取平方根的数字 - 被评估为一个非常小的负数。)
【问题讨论】:
标签: c# .net multithreading thread-safety