5

我正在寻找一个线程安全的计数器实现,它使用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

4

3 回答 3

2

托管Interlocked.CompareExchange映射直接映射到InterlockedCompareExchangeWin32 API 中(也有64 位版本)。

正如您在函数签名中看到的那样,本机 API 要求目标是 volatile 的,即使托管 API 不需要它,Joe Duffy 在他的优秀著作Concurrent Programming on Windows中推荐使用 volatile 。

于 2011-09-26T15:28:14.453 回答
2

如果我们读取一个陈旧的值,则CompareExchange不会执行交换 - 我们基本上是在说,“只有当该值确实是我们计算的基础时才执行操作。” 只要在某个时候我们得到正确的值,就可以了。如果我们一直读取相同的陈旧值,这将是一个问题,因此CompareExchange 从未通过检查,但我强烈怀疑CompareExchange内存屏障意味着至少在循环时间之后,我们将读取最新的价值。可能发生的最坏情况将是永远循环 - 重要的是我们不可能以不正确的方式更新变量。

(是的,我认为你是对的,该Total物业是狡猾的。)

编辑:换句话说:

CompareExchange(ref totalValue, computedValue, initialValue)

意思是:“如果当前状态真的是initialValue,那么我的计算是有效的,你应该把它设置为computedValue。”

当前状态可能是错误的,至少有两个原因:

  • initialValue = totalValue;分配使用了具有不同旧值的陈旧读取
  • 在那次分配totalValue 之后发生了一些变化

我们根本不需要以不同的方式处理这些情况——所以只要在某个时候我们会开始看到最新的值,就可以进行“廉价”读取……而且我相信所涉及的内存障碍inCompareExchange将确保当我们循环时,我们看到的陈旧值只会与前一次CompareExchange调用一样陈旧。

编辑:澄清一下,我认为样本是正确的,当且仅当 CompareExchange构成相对于totalValue. 如果它没有——如果我们仍然可以读取任意旧值,totalValue当我们继续循环时——那么代码确实被破坏了,并且可能永远不会终止。

于 2011-09-26T14:40:22.603 回答
1

编辑:

这么长时间后有人给了我一个赞成票,所以我重新阅读了问题和答案,发现了一个问题。

我要么不知道介绍读物,要么没有想到。假设 Interlocked.CompareExchange 没有引入任何障碍(因为它没有在任何地方记录),编译器可以将您的AddToTotal方法转换为以下损坏的版本,其中最后两个参数Interlocked.CompareExchange可以看到不同的totalValue值!

public int AddToTotal(int addend)
{
    int initialValue;
    do
    {        
        initialValue = totalValue;
    } while (initialValue != Interlocked.CompareExchange(
        ref totalValue, totalValue + addend, totalValue));

    return initialValue + addend;
}

因此,您可以使用Volatile.Read. 在 x86 上,Volatile.Read无论如何只是标准读取(它只是防止编译器重新排序),所以没有理由不这样做。那么编译器应该能够做的最坏的事情是:

public int AddToTotal(int addend)
{
    int initialValue;
    do
    {
        initialValue = Volatile.Read (ref totalValue);
    } while (initialValue != Interlocked.CompareExchange(
        ref totalValue, initialValue + addend, initialValue));

    return initialValue + addend;
}

不幸的是,Eric Lippert 曾经声称 volatile read 并不能保证防止引入的读取。我真的希望他是错的,因为这意味着很多低锁代码几乎不可能在 C# 中正确编写。他自己确实在某处提到他不认为自己是低级同步方面的专家,所以我只是假设他的陈述是不正确的,并希望最好。


原答案:

与流行的误解相反,获取/释放语义并不能确保从共享内存中获取新值,它们只会影响具有获取/释放语义的其他内存操作的顺序。每个内存访问必须至少与上次获取读取一样新,并且最多与下一次发布写入一样陈旧。(类似于内存屏障。)

在这段代码中,您只需要担心一个共享变量:totalValue. CompareExchange 是一个原子 RMW 操作这一事实足以确保它所操作的变量将得到更新。这是因为原子 RMW 操作必须确保所有处理器都同意变量的最新值是什么。

关于Total你提到的其他属性,它是否正确取决于它的要求。几点:

  • int保证是原子的,所以你总是会得到一个有效的值(从这个意义上说,你显示的代码可以被视为“正确的”,如果只需要一些有效的,可能是陈旧的值)
  • 如果没有获取语义Volatile.Read的读取(或读取volatile int)意味着在它之后写入的所有内存操作实际上可能发生在之前(读取操作较旧的值并且写入在它们应该之前对其他处理器可见)
  • 如果不使用原子 RMW 操作来读取(如Interlocked.CompareExchange(ref x, 0, 0)),则接收到的值可能不是某些其他处理器视为最新值的值
  • 如果需要关于其他内存操作的最新值和排序,则Interlocked.CompareExchange 应该可以工作(底层 WinAPIInterlockedCompareExchange使用完整的屏障,不太确定 C# 或 .Net 规范)但如果您希望确定,您可以添加一个显式读取后的内存屏障
于 2018-12-05T08:49:41.663 回答