This is the second in a series of blog posts I'm writing on how we might fix the mess that is .NET atomics.
- Untangling .NET Atomics: Defining Terms
- Untangling .NET Atomics: The Status Quo (you are here)
- Untangling .NET Atomics: The Problems (in progress)
Now that (hopefully) everyone is on the same page regarding terminology, it's time to take a look at the atomics functionality currently provided by the .NET ecosystem.
C# compiles to Common Intermediate Language (CIL), the bytecode of the .NET ecosystem. CIL has a particular prefix instruction called
volatile. which, when combined with load and store instructions, imposes the volatility semantics discussed in the previous post. So, loads get acquire semantics and stores get release semantics, and both are guarded against optimizations. This prefix can be applied to instructions such as
stind.i8, etc. Some CIL instructions (e.g.
cpblk) perform both loads and stores as part of their operation - in these cases, a single
volatile. prefix applies to both the load and the store, and imposes acquire and release semantics on these, respectively.
The C# language features a
volatile keyword that partially exposes the functionality of the CIL
volatile. instruction. When applied to a field,
volatile will make the C# compiler emit, for example,
volatile.ldfld instead of just
ldfld when loading from a non-
static field, 0r
volatile.stsfld instead of just
stsfld when storing to a
volatile keyword can only be applied to fields whose types are either 32 bits wide or as wide as the system's word size. We'll get into the pros and cons of that in the next post.
Volatile Class Methods
System.Threading.Volatile class is the most recent attempt to expose understandable atomics functionality in the framework, and it is relatively successful. This class exposes a bunch of
Write methods which perform loads and stores with acquire and release semantics, respectively. In addition, all of these are guaranteed to be atomic - even the 64-bit overloads on 32-bit systems. Of course, this muddies the waters yet again in regards to terminology, since volatility doesn't necessarily have anything to do with atomicity, but alas.
Interlocked Class Methods
System.Threading.Interlocked class is where a lot of the really interesting atomics functionality exists:
MemoryBarrier, etc. For all of the methods on this class, atomicity and sequential consistency are guaranteed. In other words, these methods are the easiest to understand and use out of all the functionality discussed so far. I won't go into full detail on what all of these do as this is fairly easy to find in the relevant documentation, but a few deserve special mention.
Read, which only has 64-bit overloads, exists specifically to enable atomic loads of 64-bit values on 32-bit systems. It's similar to 64-bit overloads of
Volatile.Read, but with the stronger sequential consistency semantics. (If you're wondering why an equivalent
Write method doesn't exist, it's because the 64-bit overloads of
Exchange effectively fulfill the same purpose.)
MemoryBarrier inserts a full memory barrier with sequential consistency semantics.
MemoryBarrierProcessWide is an interesting beast. It inserts a full memory barrier just like
MemoryBarrier but also forces all other threads in the process to immediately execute one as well, and then returns. This method can be hard to grok – I'll probably write a whole post about it later – but for now, what you need to know is that it is very expensive compared to
MemoryBarrier and should generally be avoided except in some very niche situations where it's extremely useful.
Thread Class Methods
System.Threading.Thread class provides a number of
VolatileWrite methods to perform reads and writes. Confusingly, in .NET Framework, these methods used to have sequential consistency semantics, despite what the names suggest. In .NET 5+, they simply delegate to the methods on the
System.Threading.Volatile class, so they instead have acquire or release semantics, respectively. (This means that any code using these methods could break when ported from .NET Framework 4.x to .NET 5+.)
There is also a
MemoryBarrier method which simply delegates to the same method on the
Next, I'll cover the many issues with the functionality described here.