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

发布于 2024-12-06 23:50:37 字数 1320 浏览 0 评论 0原文

我正在寻找使用支持按任意值递增的 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 调用开始,那么这段代码就可以了。但这有保证吗?

I was looking for a thread-safe counter implementation using Interlocked that supported incrementing by arbitrary values, and found this sample straight from the Interlocked.CompareExchange documentation (slightly changed for simplicity):

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; }
 }

I get what this code is trying to do, but I'm not sure how it can get away with not using a volatile read of the shared variable when assigning to the temporary variable that is added to.

Is there a chance that initialValue will hold a stale value throughout the loop, making the function never return? Or does the memory-barrier (?) in CompareExchange eliminate any such possibility? Any insight would be appreciated.

EDIT: I should clarify that I understand that if CompareExchange caused the subsequent read of totalValue to be up to date as of the last CompareExchange call, then this code would be fine. But is that guaranteed?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

你对谁都笑 2024-12-13 23:50:37

如果我们读取过时的值,那么 CompareExchange 将不会执行交换 - 我们基本上是在说,“仅当该值确实是我们计算所依据的值时才进行操作。 ”只要在某个点我们得到正确的值,就可以了。如果我们一直读取相同的过时值,那将是一个问题,因此 CompareExchange 从未 通过检查,但我强烈怀疑 CompareExchange内存屏障意味着至少在循环结束后,我们将读取最新值。最糟糕的情况可能是永远循环 - 重要的是我们不可能以错误的方式更新变量。

(是的,我认为您是对的,Total 属性是不可靠的。)

编辑:换句话说:

CompareExchange(ref totalValue, computedValue, initialValue)

意思是:“如果当前状态确实是initialValue ,那么我的计算是有效的,您应该将其设置为 compulatedValue。”

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

  • initialValue = TotalValue; 赋值使用了具有不同旧值的过时读取
  • totalValue 之后发生了一些变化em> 该作业

我们根本不需要以不同的方式处理这些情况 - 所以只要在某个点我们开始看到最新的内容,就可以进行“廉价”阅读价值观......我相信涉及的记忆障碍CompareExchange 将确保当我们循环时,我们看到的过时值仅与之前的 CompareExchange 调用一样过时。

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

If we read a stale value, then the CompareExchange won't perform the exchange - we're basically saying, "Only do the operation if the value really is the one we've based our calculation on." So long as at some point we get the right value, it's fine. It would be a problem if we kept reading the same stale value forever, so CompareExchange never passed the check, but I strongly suspect that the CompareExchange memory barriers mean that at least after the time through the loop, we'll read an up-to-date value. The worst that could happen would be cycling forever though - the important point is that we can't possibly update the variable in an incorrect way.

(And yes, I think you're right that the Total property is dodgy.)

EDIT: To put it another way:

CompareExchange(ref totalValue, computedValue, initialValue)

means: "If the current state really was initialValue, then my calculations are valid and you should set it to computedValue."

The current state could be wrong for at least two reasons:

  • The initialValue = totalValue; assignment used a stale read with a different old value
  • Something changed totalValue after that assignment

We don't need to handle those situations differently at all - so it's fine to do a "cheap" read so long as at some point we'll starting seeing up-to-date values... and I believe the memory barriers involved in CompareExchange will ensure that as we loop round, the stale value we see is only ever as stale as the previous CompareExchange call.

EDIT: To clarify, I think the sample is correct if and only if CompareExchange constitutes a memory barrier with respect to totalValue. If it doesn't - if we can still read arbitrarily-old values of totalValue when we keep going round the loop - then the code is indeed broken, and may never terminate.

满身野味 2024-12-13 23:50:37

编辑:

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

我要么不知道介绍读物,要么我没有想到过。假设 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 曾经声称易失性读取并没有'不能保证防止引入读取。我真心希望他是错的,因为这意味着许多低锁代码几乎不可能用 C# 正确编写。他本人确实在某处提到过,他不认为自己是低级同步方面的专家,所以我只是认为他的说法是不正确的,并希望得到最好的结果。


原始答案:

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

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

关于您提到的另一个Total属性,它是否正确取决于它的要求。一些要点:

  • int 保证是原子的,因此您将始终获得有效值(从这个意义上说,您显示的代码可以被视为“正确”,如果除了 需要一些有效的、可能是过时的值)
  • 如果在没有获取语义的情况下读取(Volatile.Read 或读取 易失性 int),则 意味着之后写入的所有内存操作它实际上可能发生在之前(读取旧值并写入提前对其他处理器可见)
  • 如果不使用原子 RMW 操作来读取(例如 Interlocked.CompareExchange(ref x, 0, 0)),接收到的值可能与其他处理器不同 则将其视为最新值
  • 如果需要最新值和其他内存操作的排序, ,Interlocked.CompareExchange 应该工作(底层 WinAPI 的InterlockedCompareExchange 使用完整屏障,不太确定 C# 或 .Net 规范),但如果您希望确定,可以在读取后添加显式内存屏障

Edit:

Someone gave me an upvote after all this time so I re-read the question and the answer and noticed a problem.

I either didn't know about introduced reads or it hasn't crossed my mind. Assuming Interlocked.CompareExchange doesn't introduce any barriers (since it's not documented anywhere), the compiler is allowed to transform your AddToTotal method into the following broken version, where the last two arguments to Interlocked.CompareExchange could see different totalValue values!

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

    return initialValue + addend;
}

For this reason, you can use Volatile.Read. On x86, Volatile.Read is just a standard read anyway (it just prevents compiler reorderings) so there's no reason not to do it. Then the worst that the compiler should be able to do is:

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;
}

Unfortunately, Eric Lippert once claimed volatile read doesn't guarantee protection against introduced reads. I seriously hope he's wrong because that would mean lots of low-lock code is almost impossible to write correctly in C#. He himself did mention somewhere that he doesn't consider himself an expert on low-level synchronization so I just assume his statement was incorrect and hope for the best.


Original answer:

Contrary to popular misconception, acquire/release semantics don't ensure a new value gets grabbed from the shared memory, they only affect the order of other memory operations around the one with acquire/release semantics. Every memory access must be at least as recent as the last acquire read and at most as stale as the next release write. (Similar for memory barriers.)

In this code, you only have a single shared variable to worry about: totalValue. The fact that CompareExchange is an atomic RMW operation is enough to ensure that the variable it operates on will get updated. This is because atomic RMW operations must ensure all processors agree on what the most recent value of the variable is.

Regarding the other Total property you mentioned, whether it's correct or not depends on what is required of it. Some points:

  • int is guaranteed to be atomic, so you will always get a valid value (in this sense, the code you've shown could be viewed as "correct", if nothing but some valid, possibly stale value is required)
  • if reading without acquire semantics (Volatile.Read or a read of volatile int) means that all memory operations written after it may actually happen before (reads operating on older values and writes becoming visible to other processors before they should)
  • if not using an atomic RMW operation to read (like Interlocked.CompareExchange(ref x, 0, 0)), a value received may not be what some other processors see as the most recent value
  • if both the freshest value and ordering in regards to other memory operations is required, Interlocked.CompareExchange should work (the underlying WinAPI's InterlockedCompareExchange uses a full barrier, not so sure about C# or .Net specifications) but if you wish to be sure, you could add an explicit memory barrier after the read
半寸时光 2024-12-13 23:50:37

托管 Interlocked.CompareExchange 直接映射到 InterlockedCompareExchange (还有一个 64 位版本)。

正如您在函数签名中所看到的,本机 API 要求目标是易失性的,尽管托管 API 不要求这样做,但 Joe Duffy 在他的优秀著作 Windows 上的并发编程

The managed Interlocked.CompareExchange maps directly to the InterlockedCompareExchange in the Win32 API (there is also a 64 bit version).

As you can see in the function signatures, the native API requires the destination to be volatile and, even though it is not required by the managed API, using volatile is recommended by Joe Duffy in his excellent book Concurrent Programming on Windows.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文