这个MSDN CompareExchange示例如何不需要易失性读取?

时间:2011-09-26 14:33:08

标签: c# .net multithreading volatile interlocked

我正在寻找使用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调用,那么这段代码就可以了。但这有保证吗?

3 个答案:

答案 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),则意味着在此之后写入的所有内存操作可能实际上都在此之前发生(读取对较旧的值进行操作,并且写入在其他处理器之前变为可见)他们应该)
  • 如果不使用原子RMW操作进行读取(如Interlocked.CompareExchange(ref x, 0, 0)),则接收到的值可能不是其他处理器认为是最新的值
  • 如果同时需要有关其他内存操作的最新值和顺序,则Interlocked.CompareExchange 应该有效(底层WinAPI的InterlockedCompareExchange使用了完整的屏障,所以不能肯定有关C#或.Net规范的信息),但如果您希望确定的话,可以在读取后添加显式的内存屏障