Interlocked.CompareExchange
I've got a hammer and you're all f**king nails
Interlocked.CompareExchange
is magic. No, seriously.
Yes, it does a comparison and replaces a value if the comparison is true. In all cases, it returns the old value. Why is that so interesting?
Well, you can read a value: value = Interlocked.CompareExchange(ref _location, null, null)
. If _location
was already null
, it will compare to the first null
, succeed, and replace it with the second null
, and then return the original null
. Therefore the value is not modified.
You can update a value only if it wasn't modified by someone else: Interlocked.CompareExchange(ref _location, oldValueSeenEarlier, newValue)
. The update only happens if _location == oldValueSeenEarlier
.
So far, it doesn't seem that interesting. The real magic is that this is one of the few method calls in the entirety of C# that maps directly to a processor instruction CMPXCHG8B
(for 64-bit x64). The processor itself ensures that no other cores can update the value from underneath you. If the comparison is true, the value will be replaced in one atomic operation. Even the operating system can't interrupt it; your thread gets pre-empted either before or after the instruction but not during it. That means you can't have the situation where if(_location == oldValueSeenEarlier)
runs, evaluates to true
, then another thread changed _location
, while you blindly overwrite that value with yours.
However we can do something even more interesting here. We can use this to locklessly merge changes among threads, so long as the operations we want to merge don't have side-effects. That's one of the reasons functional languages are much easier to parallelize and why all your compsci professors ranted against the very idea of side-effects.
Func<T, T> mutator = ...
var comparand = _value;
while(true) {
if(Interlocked.CompareExchange(ref _value,
comparand,
mutator(comparand))
== comparand) {
break;
}
comparand = _value;
}
Here we loop, each time checking to see if the value that was in _value
is unchanged. If it is unchanged, we know the exchange worked and _value
now holds the result of mutator(comparand)
so we break.
If _value
was something else, then the mutated object is garbage since it doesn't contain new changes in _value
. If we are using immutable snapshots, we can say the snapshots have diverged, so we throw our snapshot away.
So long as mutator()
is side-effect-free, we can simply grab the new value in _value
, re-apply the mutation, and attempt the exchange again. We are re-running the transaction, but on the new data. It may take several iterations under heavy load, but it will eventually succeed. You can get fancy and add back-off timers and maximum loop counts, but in practice I haven't found a need for that yet.
Part 1: Russell's Rules For Highly Concurrent Software
Part 2: Immutability Part 2
Part 3: ContextLocal
Part 4: On Rediscovering the Classic Coordinated Transaction Problem
Part 6: Immutability and Thread Safety
This blog represents my own personal opinion and is not endorsed by my employer.