What Interlocked.CompareExchange is used for in the dapper .net method?

.net c# dapper thread-safety

Question

In dapper code in Link.TryAdd method, there is the following piece of code:

var snapshot = Interlocked.CompareExchange(ref head, null, null);

Why is this required instead of simple:

var snapshot = head;

both lines do not change the value of head, both lines assign the value of head to snapshot. Why the first one was chosen over the second?

Edit: the code I'm referring to is here: https://github.com/SamSaffron/dapper-dot-net/blob/77227781c562e65c167bf7a933d69291d5bdc6f3/Dapper/SqlMapper.cs

Accepted Answer

They want to do a volatile read however there is no overload of Thread.VolatileRead that takes a generic type parameter. Using Interlocked.CompareExchange this way achieves the same result.

The problem they are trying to solve is that the JIT compiler can optimize away the assignment to a temp if it sees fit. This can cause threading problems if another thread mutates the head reference while the current thread is using it in a sequence of operations.

Edit:

The issue is not that a stale value is read at beginning of TryAdd. The problem is that on line 105 they need to compare the current head to the previous head (held in snapshot). If there is an optimization then there is no snapshot variable holding the previous value and head is read again at that point. It is very likely that CompareExchange succeeds even though head might have changed between lines 103 and 105. The result is that a node in the list is lost if two threads call TryAdd simultaneously.


Popular Answer

mike z is right: This is preventing a (legal) JIT optimization that would break the code.

They could have used the volatile struct trick, though: Read head and assign it to a volatile field of some struct. Next, read it from that field and it is guaranteed to be a volatile read!

The struct itself doesn't matter at all. All that matters is that a volatile field was used to copy the variable through.

Like that:

struct VolatileHelper<T> { public volatile T Value; }
...
var volatileHelper = new VolatileHelper<Field>();
volatileHelper.Value = head;
var snapshot = volatileHelper.Value;

Hopefully, it has no runtime cost. In any case, the cost is less than an interlocked operation which is causing CPU memory coherency traffic.

Actually, the fact that every cache access (even a reading one) requires memory coherency traffic makes this a slow cache! Interlocked operations are a system global resource that does not scale with more CPUs. An Interlocked access uses a global hardware lock (per memory address, but there is only one address here).




Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why