这个 MSDN CompareExchange 示例如何不需要易失性读取?
我正在寻找使用支持按任意值递增的 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
如果我们读取过时的值,那么
CompareExchange
将不会执行交换 - 我们基本上是在说,“仅当该值确实是我们计算所依据的值时才进行操作。 ”只要在某个点我们得到正确的值,就可以了。如果我们一直读取相同的过时值,那将是一个问题,因此CompareExchange
从未 通过检查,但我强烈怀疑CompareExchange
内存屏障意味着至少在循环结束后,我们将读取最新值。最糟糕的情况可能是永远循环 - 重要的是我们不可能以错误的方式更新变量。(是的,我认为您是对的,
Total
属性是不可靠的。)编辑:换句话说:
意思是:“如果当前状态确实是
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, soCompareExchange
never passed the check, but I strongly suspect that theCompareExchange
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:
means: "If the current state really was
initialValue
, then my calculations are valid and you should set it tocomputedValue
."The current state could be wrong for at least two reasons:
initialValue = totalValue;
assignment used a stale read with a different old valuetotalValue
after that assignmentWe 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 previousCompareExchange
call.EDIT: To clarify, I think the sample is correct if and only if
CompareExchange
constitutes a memory barrier with respect tototalValue
. If it doesn't - if we can still read arbitrarily-old values oftotalValue
when we keep going round the loop - then the code is indeed broken, and may never terminate.编辑:
有人给了我一个赞成票,所以我重新阅读了问题和答案,发现了一个问题。
我要么不知道介绍读物,要么我没有想到过。假设 Interlocked.CompareExchange 不会引入任何障碍(因为它没有在任何地方记录),则允许编译器将您的
AddToTotal
方法转换为以下损坏的版本,其中最后一个Interlocked.CompareExchange
的两个参数可能会看到不同的totalValue
值!因此,您可以使用
Volatile.Read
。在 x86 上,Volatile.Read
无论如何都只是标准读取(它只是阻止编译器重新排序),因此没有理由不这样做。那么编译器应该能够做的最糟糕的事情是:不幸的是,Eric Lippert 曾经声称易失性读取并没有'不能保证防止引入读取。我真心希望他是错的,因为这意味着许多低锁代码几乎不可能用 C# 正确编写。他本人确实在某处提到过,他不认为自己是低级同步方面的专家,所以我只是认为他的说法是不正确的,并希望得到最好的结果。
原始答案:
与流行的误解相反,获取/释放语义并不能确保从共享内存中获取新值,它们只会影响周围其他内存操作的顺序具有获取/释放语义的一个。每次内存访问必须至少与上次获取读取一样新,最多与下一次释放写入一样陈旧。 (与内存屏障类似。)
在此代码中,您只需担心一个共享变量:
totalValue
。事实上,CompareExchange 是一个原子 RMW 操作,足以确保其操作的变量会得到更新。这是因为原子 RMW 操作必须确保所有处理器都同意变量的最新值是什么。关于您提到的另一个Total属性,它是否正确取决于它的要求。一些要点:
int
保证是原子的,因此您将始终获得有效值(从这个意义上说,您显示的代码可以被视为“正确”,如果除了 需要一些有效的、可能是过时的值)Volatile.Read
或读取易失性 int
),则 意味着之后写入的所有内存操作它实际上可能发生在之前(读取旧值并写入提前对其他处理器可见)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 toInterlocked.CompareExchange
could see differenttotalValue
values!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: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)Volatile.Read
or a read ofvolatile 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)Interlocked.CompareExchange(ref x, 0, 0)
), a value received may not be what some other processors see as the most recent valueInterlocked.CompareExchange
should work (the underlying WinAPI'sInterlockedCompareExchange
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托管
Interlocked.CompareExchange
直接映射到InterlockedCompareExchange
(还有一个 64 位版本)。正如您在函数签名中所看到的,本机 API 要求目标是易失性的,尽管托管 API 不要求这样做,但 Joe Duffy 在他的优秀著作 Windows 上的并发编程。
The managed
Interlocked.CompareExchange
maps directly to theInterlockedCompareExchange
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.