阅读 Jeffrey Richter 在 CLR 中通过 C# 第 4 版创造的 Interlocked Anything 模式时,他给出了以下示例,证明我们可以为
Add
做的不仅仅是
Interlocked
public static Int32 Maximum(ref Int32 target, Int32 value)
{
Int32 currentVal = target, startVal, desiredVal;
// Don't access target in the loop except in an attempt
// to change it because another thread may be touching it
do
{
// Record this iteration's starting value
startVal = currentVal;
// Calculate the desired value in terms of startVal and value
desiredVal = Math.Max(startVal, value);
// NOTE: the thread could be preempted here!
// if (target == startVal) target = desiredVal
// Value prior to potential change is returned
currentVal = Interlocked.CompareExchange(ref target, desiredVal, startVal);
// If the starting value changed during this iteration, repeat
} while (startVal != currentVal);
// Return the maximum value when this thread tried to set it
return desiredVal;
}
对于 C# 编译器为事件生成的
add
和 remove
方法,可以找到非常相似的模式版本:
[CompilerGenerated]
{
EventHandler eventHandler = this.MyEvent;
EventHandler eventHandler2;
do
{
eventHandler2 = eventHandler;
EventHandler value2 = (EventHandler)Delegate.Combine(eventHandler2, value);
eventHandler = Interlocked.CompareExchange(ref this.MyEvent, value2, eventHandler2);
}
while ((object)eventHandler != eventHandler2);
}
然而,一个很大的区别是我们如何获取“目标”值 - ref 参数与字段访问。
使用 ref 参数的原始示例,在通过以下方式将目标值分配给临时值之前如何防止目标值发生更改:
Int32 currentVal = target, startVal, desiredVal;
例如,当我们跳转到方法的开头时,另一个线程更改值?
这里的关键点是
Interlocked.CompareExchange
;这是一种非常著名的模式,其中代码获取当前值的快照,计算预期的新值,然后使用 Interlocked.CompareExchange
执行条件更新 且仅当 目标与我们的快照匹配,即该值没有改变。如果它发生了变化,那么我们将使用更新的值快照重复从第一个主体开始的所有操作。因此,代码将不会离开此循环,直到 if 设法完成一次迭代,其中值在快照和更新之间不会更改。
线程安全很复杂,您需要指定针对什么风险代码是安全的;我们可以可靠地说,这种机制确保我们永远不会因为未观察到的冲突更新而出现错误 - 例如,由于这是一个最大值操作,因此尽管另一个线程更新该值,我们也永远不会从 7 变为 14到 25(导致 25 被错误地丢弃)。所以;线程安全避免碰撞:是的。然而,如果我们是迂腐的,我们就不得不承认,无法保证这可能需要多长时间 - 理论上,如果并发之神如此希望的话,它可能会永远循环 - 或者我猜最多 40 亿次,因为它是一个32位整数。所以从另一个角度来说,它很容易受到碰撞副作用的影响而影响性能。实际上,这几乎从来都不是一个真正的问题,在大多数常见用例中,甚至循环两次也不太可能。当它是一个真正的问题时,通常意味着某些内容更新过于频繁,可以通过让工作循环仅定期而不是每次迭代更新共享令牌来避免这种情况。