这个MSDN CompareExchange示例如何不需要volatile读取?

本文关键字:不需要 volatile 读取 MSDN CompareExchange 这个 | 更新日期: 2023-09-27 18:09:56

我正在寻找一个使用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中的内存屏障(?)是否消除了任何这种可能性?如有任何见地,不胜感激。

EDIT:我应该澄清一下,我理解如果CompareExchange导致totalValue后续读取在最后 CompareExchange调用时是最新的,那么此代码将是好的。但这能保证吗?

这个MSDN CompareExchange示例如何不需要volatile读取?

如果我们读取一个过时的值,那么CompareExchange将不会执行交换—我们基本上是在说,"只有当这个值确实是我们计算所基于的值时才会执行操作。"只要在某个点得到正确的值,就可以。如果我们一直读取相同的过期值,那么CompareExchange 永远不会通过检查,这将是一个问题,但我强烈怀疑CompareExchange内存屏障意味着至少在通过循环的时间之后,我们将读取最新值。最坏的情况可能是永远循环——重要的是我们不可能以不正确的方式更新变量。

(是的,我认为你是对的,Total属性是可疑的。)

编辑:换句话说:

CompareExchange(ref totalValue, computedValue, initialValue)

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

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

  • initialValue = totalValue;赋值使用了一个具有不同旧值的陈旧读取
  • 在之后totalValue 改变了赋值

我们根本不需要以不同的方式处理这些情况-所以只要在某个点上我们开始看到最新的值,就可以进行"廉价"读取。而且我相信CompareExchange中涉及的内存屏障将确保当我们循环时,我们看到的陈旧值只和以前的CompareExchange调用一样陈旧。

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

Edit:

有人给了我一个赞,所以我重读了问题和答案,发现了一个问题。

我要么不知道介绍阅读,要么我从没想过。假设联锁。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.Readvolatile int的读取)意味着在它之后写入的所有内存操作实际上都可能发生在之前(对旧值的读取操作和写入操作在其他处理器之前变得可见)
  • 如果不使用原子RMW操作来读取(如Interlocked.CompareExchange(ref x, 0, 0)),则接收到的值可能不是其他处理器看到的最近的值
  • 如果需要最新值和其他内存操作的排序,Interlocked.CompareExchange 应该工作(底层WinAPI的InterlockedCompareExchange使用完整的屏障,不太确定c#或。net规范),但如果你想确定,你可以在读取
  • 后添加显式的内存屏障。

管理的Interlocked.CompareExchange直接映射到Win32 API中的InterlockedCompareExchange(也有64位版本)。

在函数签名中可以看到,本机API要求目标是volatile的,尽管托管API不要求,但Joe Duffy在他的优秀著作《Windows上的并发编程》中推荐使用volatile。