Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference between Threading.Volatile.Read(Int64) and Threading.Interlocked.Read(Int64)?

What is the difference, if any, of the Read(Int64) method of the .NET system classes System.Threading.Volatile and System.Threading.Interlocked?

Specifically, what are their respective guarantees / behaviour with regard to (a) atomicity and (b) memory ordering.

Note that this is about the Volatile class, not the volatile (lower case) keyword.


The MS docs state:

Volatile.Read Method

Reads the value of a field. On systems that require it, inserts a memory barrier that prevents the processor from reordering memory operations as follows: If a read or write appears after this method in the code, the processor cannot move it before this method.

...

Returns Int64

The value that was read. This value is the latest written by any processor in the computer, regardless of the number of processors or the state of processor cache.

vs.

Interlocked.Read(Int64) Method

Returns a 64-bit value, loaded as an atomic operation.

Particularly confusing seems that the Volatile docs do not talk about atomicity and the Interlocked docs do not talk about ordering / memory barriers.

Side Note: Just as a reference: I'm more familiar with the C++ atomic API where atomic operations always also specify a memory ordering semantic.


The question link (and transitive links) helpfully provided by Pavel do a good job of explaining the difference / ortogonality of volatile-as-in-memory-barrier and atomic-as-in-no-torn-reads, but they do not explain how the two concepts apply to these two classes.

  • Does Volatile.Read make any guarantees about atomicity?
  • Does Interlocked.Read (or, really, any of the Interlocked functions) make any guarantees about memory order?
like image 280
Martin Ba Avatar asked Aug 15 '19 09:08

Martin Ba


2 Answers

Interlocked.Read translates into a CompareExchange operation:

public static long Read(ref long location)
{
    return Interlocked.CompareExchange(ref location, 0, 0);
}

Therefore it has all the benefits of CompareExchange:

  • Full memory barrier
  • Atomicity

Volatile.Read on the other hand has only acquire semantics. It helps you ensuring the execution order of your read operations, without any atomicity or freshness guarantee.

like image 199
Kevin Gosse Avatar answered Oct 16 '22 11:10

Kevin Gosse


The documentation of the Volatile.Read(long) method doesn't mention anything about atomicity, but the source code is quite revealing:

private struct VolatileIntPtr { public volatile IntPtr Value; }

[Intrinsic]
[NonVersionable]
public static long Read(ref long location) =>
#if TARGET_64BIT
    (long)Unsafe.As<long, VolatileIntPtr>(ref location).Value;
#else
    // On 32-bit machines, we use Interlocked, since an ordinary volatile read would not be atomic.
    Interlocked.CompareExchange(ref location, 0, 0);
#endif
  • On 32-bit machines, the Volatile.Read method invokes indirectly the Interlocked.CompareExchange, just like the Interlocked.Read does (source code), so there is no difference between the two. A full fence is emitted by both methods.
  • On 64-bit machines the atomicity of the reading is guaranteed by the CPU architecture, so a cheaper half fence is emitted instead.

So the Volatile.Read seems to be the preferable option overall. Although its atomicity is not guaranteed by the documentation, if it wasn't atomic its usefulness would be severely limited, if any. What use would you have for a value that can be potentially torn?

Note: the Intrinsic attribute means that the code of the decorated method can be potentially replaced/optimized by the Jitter. This can be slightly concerning, so please make your own judgement about whether it's safe to use the Volatile.Read for reading long values in a multithreaded environment.

like image 2
Theodor Zoulias Avatar answered Oct 16 '22 10:10

Theodor Zoulias