我正在寻找使用Interlocked
的线程安全计数器实现,它支持按任意值递增,并直接从Interlocked.CompareExchange
文档中找到此示例(为简单起见略有改动):
private int totalValue = 0;
public int AddToTotal(int addend)
{
int initialValue, computedValue;
do
{
// How can we get away with not using a volatile read of totalValue here?
// Shouldn't we use CompareExchange(ref TotalValue, 0, 0)
// or Thread.VolatileRead
// or declare totalValue to be volatile?
initialValue = totalValue;
computedValue = initialValue + addend;
} while (initialValue != Interlocked.CompareExchange(
ref totalValue, computedValue, initialValue));
return computedValue;
}
public int Total
{
// This looks *really* dodgy too, but isn't
// the target of my question.
get { return totalValue; }
}
我得到了这段代码试图做的事情,但我不确定在分配给添加的临时变量时,如何不使用共享变量的易失性读取。
initialValue
是否有可能在整个循环中保持陈旧值,使函数永不返回?或CompareExchange
中的记忆障碍(?)是否消除了这种可能性?任何见解都将不胜感激。
编辑:我应该澄清,我了解如果CompareExchange
导致totalValue
的后续读取是最新的上次 CompareExchange
调用,那么这段代码就可以了。但这有保证吗?
答案 0 :(得分:2)
如果我们读取陈旧的值,那么CompareExchange
将不会执行交换 - 我们基本上会说,“只有在值确实是我们基于计算的值时才进行操作。 “只要在某些点我们得到正确的值,就可以了。如果我们永远读取相同的陈旧值,那将是一个问题,因此CompareExchange
从未通过检查,但我强烈怀疑CompareExchange
内存障碍意味着至少在循环开始之后,我们将读取最新值。可能发生的最坏情况将是永远循环 - 重要的是我们不可能以不正确的方式更新变量。
(是的,我认为Total
属性狡猾是对的。)
编辑:换句话说:
CompareExchange(ref totalValue, computedValue, initialValue)
表示:“如果当前状态确实为initialValue
,那么我的计算有效,您应将其设置为computedValue
。”
目前的状态可能是错误的,至少有两个原因:
initialValue = totalValue;
作业使用了具有不同旧值的陈旧阅读totalValue
我们根本不需要以不同的方式处理这些情况 - 所以只要在某些点我们就会开始看到最新值,这样就可以进行“便宜”阅读......我相信CompareExchange
中涉及的内存障碍将确保当我们循环时,我们看到的陈旧值只会像之前的CompareExchange
调用一样陈旧。
编辑:为了澄清,我认为样本是正确的当且仅当 CompareExchange
构成totalValue
的内存障碍时。如果没有 - 如果我们仍然可以在循环中读取totalValue
的任意旧值 - 那么代码确实被破坏了,并且可能永远不会终止。
答案 1 :(得分:2)
托管Interlocked.CompareExchange
直接映射到Win32 API中的InterlockedCompareExchange
(还有64 bit version)。
正如您在函数签名中看到的那样,本机API要求目标是易变的,即使托管API不需要,但是Joe Duffy在他出色的书Concurrent Programming on Windows中建议使用volatile。
答案 2 :(得分:0)
与普遍的误解相反,获取/释放语义不能确保从共享内存中获取新的值,它们只会影响 other 内存操作的顺序(具有获取/释放语义) 。每次内存访问必须至少与上次获取读操作一样近,并且与下一次发布写操作最多一样陈旧。 (类似于记忆障碍。)
在此代码中,您只有一个共享变量要担心:totalValue
。 CompareExchange是原子RMW操作,这一事实足以确保对其操作的变量进行更新。这是因为原子RMW操作必须确保所有处理器都同意变量的最新值。
关于您提到的其他Total
属性,它是否正确取决于对它的要求。一些要点:
int
被保证是原子的,因此您将始终获得有效的值(从这个意义上讲,您显示的代码可以被视为“正确的”,如果除了 some em>有效,可能需要过时的值)Volatile.Read
或读取volatile int
),则意味着在此之后写入的所有内存操作可能实际上都在此之前发生(读取对较旧的值进行操作,并且写入在其他处理器之前变为可见)他们应该)Interlocked.CompareExchange(ref x, 0, 0)
),则接收到的值可能不是其他处理器认为是最新的值Interlocked.CompareExchange
应该有效(底层WinAPI的InterlockedCompareExchange
使用了完整的屏障,所以不能肯定有关C#或.Net规范的信息),但如果您希望确定的话,可以在读取后添加显式的内存屏障