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

2

14

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?
Triangle answered 15/8, 2019 at 9:17 Comment(3)
Pro'lly relevant: #12435825Triangle
This thread is also can be helpful, with reference to Eric Lippert articlesGerda
@PavelAnikhouski - thanks for the link. It's a good one, and sheds some light on to the concepts, but it doesn't IMO, really answer the question with regard to the two concrete .NET classes.Triangle
R
6

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.

Rete answered 15/8, 2019 at 11:35 Comment(5)
Thanks. Strangely enough, the .NET docs never seem to mention anything about the memory barrier. I guess one can assume that these are implemented in terms of the Win32/Intrinsic equivalents where they do state this explicitly. What do you think?Triangle
@MartinBa I assume both are implemented the same way (with a lock cmpxchg instruction), so it's fair to compare them. The .net documentation is very bad (and sometimes even wrong) when it comes to describing the memory model.Rete
Do you happen to know why there's no Interlocked.Read(ref int arg) then? Surely memory barrier + atomicity on Int32 is needed, especially on 32-bit architectures, where interlocked Int64 read would be quite complicated and thus expensive.Potential
As I understand it, that's because atomicity is guaranteed for Int32 on both 32-bit and 64-bit architectures without the need for Interlocked. Therefore you only need to use Volatile.Read(ref int arg) to get the same guarantees. I believe that on 64-bit architectures, Interlocked.Read() is equivalent to Volatile.Read(), as atomicity is already guaranteed.Dungaree
How do you get freshness guarantee in .NET if the memory is mapped read only (Interlocked.Read fails in that case)?Potential
S
2

The documentation of the Volatile.Read(long) method doesn't mention anything about atomicity, but the atomicity of all Volatile operations is explicitly guaranteed in the documentation of the Volatile class:

The Volatile class also provides read and write operations for some 64-bit types such as Int64 and Double. Volatile reads and writes on such 64-bit memory are atomic even on 32-bit processors, unlike regular reads and writes.

Also the source code of the Volatile.Read(long) is revealing:

private struct VolatileIntPtr { public volatile IntPtr Value; }

[Intrinsic]
[NonVersionable]
public static long Read(ref readonly long location) =>
#if TARGET_64BIT
    (long)Unsafe.As<long, VolatileIntPtr>(ref Unsafe.AsRef(in location)).Value;
#else
    // On 32-bit machines, we use Interlocked, since an ordinary volatile read
    // would not be atomic.
    Interlocked.CompareExchange(ref Unsafe.AsRef(in 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 should be the preferable option in most cases, unless you need the full memory barrier provided by the Interlocked APIs. The Volatile APIs normally provide half fences only, that are adequate for getting acquire / release semantics (example).

Note: the Intrinsic attribute means that the code of the decorated method can be potentially replaced/optimized by the Jitter. This could be concerning, in case the atomicity was not guaranteed explicitly in the documentation.

Suanne answered 5/6, 2022 at 14:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.