最初看起来像一个简单解决方案的问题已被证明是一个非常有趣的挑战。
我有一个类,它维护一个内部固定大小的线程安全集合(通过在所有插入和删除操作中使用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);
}
}
然后,如果我在计算lock
时对同一个对象StandardDeviation
,我认为最终会解决问题。它没有。该值仍然以NaN
的形式出现在短暂而不经常的基础上。
坦率地说,即使上面的解决方案已经工作,它也非常混乱,对我来说似乎不太容易管理。 是否有一种标准的和/或更直接的方法来实现计算值中的线程安全性?
编辑:原来我们有一个问题的例子,起初看起来似乎只有一个可能的解释,毕竟问题完全与其他问题有关。
我一丝不苟地以各种方式实现线程安全,如果可能的话,不会做出巨大的性能牺牲 - 锁定对共享值的读写(例如Sum
和Count
),在本地缓存值,并使用相同的锁对象来修改集合和更新共享值...老实说,这一切似乎都有点过分。
没有任何效果;那个邪恶的NaN
不断涌现。因此,当StandardDeviation
返回NaN
时,我决定将集合中的所有值打印到控制台...
我立即注意到,当集合中的所有值都相同时,似乎总是发生。
这是正式的:我被浮点运算烧掉了。 (所有值都相同,因此StandardDeviation
中的基数 - 即正方根被取的数字 - 被评估为一些非常小的负数。)
答案 0 :(得分:3)
我可以锁定代码中更新这些值的所有点:
protected void ItemAdded(double item) {
// ...
lock (this.CalculationLock) {
this.Sum += item;
this.Sum2 += (item * item);
}
}
然后,如果我在计算StandardDeviation时锁定同一个对象,我认为最终会解决问题。它没有。 NaN的价值仍在短暂而罕见的基础上出现。
如果这对您不起作用,我建议您错过更新方案 - 或者您有其他问题(例如Sum或Sum2偶尔会NaN
或因为某些其他而意外的值竞争条件)。
答案 1 :(得分:2)
Sum和Count是跨多个线程共享的状态。因此,所有访问必须同步,除非你(a)有一些原子变量原语,(b)非常非常小心。
最简单,最“标准”的方法是使用与同步插入和删除相同的锁。在添加或删除时,请更新Sum和Count。然后使用相同的锁来同步对StandardDeviation函数中Sum和Count的访问。
答案 2 :(得分:1)
您可以缓存整个列表,然后使用缓存版本进行操作。像这样:
var copy = currentList.ToArray();
var sum = Sum(copy)
var sum2 = Sum2(copy)
return sum * sum2... whatever
这样,您只需在执行副本(。示例中的.ToArray)时保持锁定,并且您有一组一致的数据可供使用。当然,根据数据的大小,内存要求或性能损失可能太大。
答案 3 :(得分:1)
正如您在评论中所提到的,性能在此处是一个高优先级,您应该考虑将对象的基础数据的所有访问权限与ReaderWriterLockSlim
(或者更早的ReaderWriterLock
同步)重新使用2.0 Framework)而不是Monitor
(通过lock
语句),这样计算就不会相互阻塞。
您还应该考虑测试一个缓存数据的实现,如某些人所建议的那样:它实际上可能比同步访问变量的版本更快。
答案 4 :(得分:0)
您可以在StandardDeviation方法中添加锁定,以确保不更改值。
public double StandardDeviation
{
get
{
lock (_lockObject)
{
return Math.Sqrt((Sum2 - ((Sum * Sum) / Count)) / Count);
}
}
}