.netsynchronizationvolatilememory-fencestest-and-set

.NET memory model, volatile variables, and test-and-set: what is guaranteed?


I know that the .NET memory model (on the .NET Framework; not compact/micro/silverlight/mono/xna/what-have-you) guaranteed that for certain types (most notably primitive integers and references) operations were guaranteed to be atomic.

Further, I believe that the x86/x64 test-and-set instruction (and Interlocked.CompareExchange) actually references the global memory location, so if it succeeds another Interlocked.CompareExchange would see the new value.

Finally, I believe that the volatile keyword is an instruction to the compiler to propagate reads & writes ASAP and to not reorder operations concerning this variable (right?).

This leads to a few questions:

  1. Are my beliefs above correct?
  2. Interlocked.Read does not have an overload for int, only for longs (which are 2 WORDs and thus are not normally read atomically). I always assumed that the .NET memory model guaranteed that the newest value would be seen when reading ints/references, however with processor caches, registers, etc. I'm starting to see this may not be possible. So is there a way to force the variable to be re-fetched?
  3. Is volatile sufficient to solve the above problem for integers and references?
  4. On x86/x64 can I assume that...

If there are two global integer variables x and y, both initialized to 0 that if I write:

x = 1;
y = 2;

That NO thread will see x = 0 and y = 2 (i.e. the writes will occur in order). Does this change if they are volatile?


Solution

  • Summary:

    1. The compiler is free to re-order instructions.
    2. The CPU is free to re-order instructions.
    3. Word-sized reads and writes are atomic. Arithmetic and other operations are not atomic because they involve a read, compute, then write.
    4. Word-sized reads from memory will always retrieve the newest value. But most of the time you don't know if you're actually reading from memory.
    5. A full memory barrier stops (1) and (2). Most compilers allow you to stop (1) by itself.
    6. The volatile keyword ensures you're reading from memory - (4).
    7. The interlocked operations (the lock prefix) allow multiple operations to be atomic. For example, a read + write (InterlockedExchange). Or a read + compare + write (InterlockedCompareExchange). They also act as memory barriers, so (1) and (2) are stopped. They always write to memory (obviously), so (4) is ensured.