Use MemoryBarrier()
System.Threading.Thread.MemoryBarrier()
is the right tool here. The code may look clunky, but it's faster than the other viable alternatives.
bool _isDone = false;
public bool IsDone
{
get
{
System.Threading.Thread.MemoryBarrier();
var toReturn = this._isDone;
System.Threading.Thread.MemoryBarrier();
return toReturn;
}
private set
{
System.Threading.Thread.MemoryBarrier();
this._isDone = value;
System.Threading.Thread.MemoryBarrier();
}
}
Don't use volatile
volatile
doesn't prevent an older value from being read, so it doesn't meet the design objective here. See Jon Skeet's explanation or Threading in C# for more.
Note that volatile
may appear to work in many cases due to undefined behavior, specifically a strong memory model on many common systems. However, reliance on undefined behavior can cause bugs to appear when you're running your code on other systems. A practical example of this would be if you're running this code on a Raspberry Pi (now possible due to .NET Core!).
Edit: After discussing the claim that "volatile
won't work here", it's unclear exactly what the C# spec guarantees; arguably, volatile
might be guaranteed to work, albeit it with a larger delay. MemoryBarrier()
is still the better solution since it ensures a faster commit. This behavior is explained in an example from "C# 4 in a Nutshell", discussed in "Why do I need a memory barrier?".
Don't use locks
Locks are a heavier mechanism meant for stronger process control. They're unnecessarily clunky in an application like this.
The performance hit is small enough that you probably won't notice it with light use, but it's still sub-optimal. Also, it can contribute (even if slightly) toward larger problems like thread starvation and deadlocking.
Details about why volatile doesn't work
To demonstrate the issue, here's the .NET source code from Microsoft (via ReferenceSource):
public static class Volatile
{
public static bool Read(ref bool location)
{
var value = location;
Thread.MemoryBarrier();
return value;
}
public static void Write(ref byte location, byte value)
{
Thread.MemoryBarrier();
location = value;
}
}
So, say that one thread sets _done = true;
, then another reads _done
to check if it's true
. What does that look like if we inline it?
void WhatHappensIfWeUseVolatile()
{
// Thread #1: Volatile write
Thread.MemoryBarrier();
this._done = true; // "location = value;"
// Thread #2: Volatile read
var _done = this._done; // "var value = location;"
Thread.MemoryBarrier();
// Check if Thread #2 got the new value from Thread #1
if (_done == true)
{
// This MIGHT happen, or might not.
//
// There was no MemoryBarrier between Thread #1's set and
// Thread #2's read, so we're not guaranteed that Thread #2
// got Thread #1's set.
}
}
In short, the problem with volatile
is that, while it does insert MemoryBarrier()
's, it doesn't insert them where we need them in this case.