How does this MSDN CompareExchange sample not need volatile reading?

I was looking for a thread counter implementation using Interlocked that supported incrementing with arbitrary values, and found this example directly from Interlocked.CompareExchange (changed a bit 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 this can go away unless you use a volatile read of the shared variable when assigning a temporary variable added to.

Is it likely that initialValue will store the deprecated value throughout the loop so that the function never returns? 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 reading of totalValue relevant from the last call to CompareExchange , then this code would be fine. But is it guaranteed?

+5
source share
3 answers

If we read an obsolete value, then CompareExchange will not perform the exchange - we basically say: "Perform the operation only if the value is really the one on which we based our calculations." Until at some point we get the right value, that's fine. It would be a problem if we continued to read the constant value forever, so CompareExchange never passed the check, but I strongly suspect that CompareExchange memory CompareExchange mean that at least after a while through the loop we will read the current value. The worst thing that could happen would be forever cyclical - the important thing is that we cannot update the variable in the wrong way.

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

EDIT: In other words:

 CompareExchange(ref totalValue, computedValue, initialValue) 

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

The current state may be incorrect for at least two reasons:

  • Purpose initialValue = totalValue; used an obsolete read with a different old value
  • Something changed totalValue after this assignment

We don’t need to deal with these situations in different ways - so it’s good to do a “cheap” reading until at some point we start to see updated values ​​... and I believe that the memory barriers involved in CompareExchange guarantee that in a round-robin round, the obsolete value that we see is always as outdated as the previous call to CompareExchange .

EDIT. To clarify, I believe that the pattern is correct if and only if CompareExchange represents a memory barrier to totalValue . If this is not the case - if we are still reading arbitrarily the old values ​​of totalValue , when we continue to totalValue loop, then the code is really broken and can never totalValue .

+2
source

Managed by Interlocked.CompareExchange maps directly to 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, although it is not required by the managed API, the use of volatile is recommended by Joe Duffy in his excellent book Parallel Programming on Windows .

+2
source

Contrary to a widespread misconception, the receive / release semantics do not guarantee that the new value will be captured from the shared memory; they only affect the order of other memory operations around one with the receive / release semantics. Each memory access should be at least as last as the last read and at least as obsolete as the next release. (Similarly for memory barriers.)

In this code, you only have one common variable to worry about: totalValue . The fact that CompareExchange is an atomic RMW operation is enough to make sure that the variable it is running on will be updated. This is because atomic RMW operations must ensure that all processors agree that the most recent value is a variable.

As for the other Total property you mentioned, right or wrong, depends on what is required of it. Some moments:

  • int guaranteed to be atomic, so you always get a real value (in this sense, the code you showed up could be considered “correct” if you only needed some real, possibly obsolete value)
  • if reading without getting semantics ( Volatile.Read or reading volatile int ) means that all memory operations written after this can happen earlier (reads work with older values ​​and becomes visible to other processors before they appear)
  • if you do not use the atomic RMW operation to read (for example, Interlocked.CompareExchange(ref x, 0, 0) ), the resulting value may not be the same as some other processors consider the most recent value
  • if both the most recent value and the order with respect to other memory operations are required, Interlocked.CompareExchange should work (the base WinAPI InterlockedCompareExchange uses a complete barrier, not so sure about C # or .Net specifications), but if you want to be sure, you may add an explicit memory barrier after reading
0
source

All Articles