A unprotected increment/decrement is not thread-safe - and thus not atomic between threads. (Although it might be "atomic" wrt the actual IL/ML transform1.)
This LINQPad example code shows unpredictable results:
void Main()
{
int nWorkers = 10;
int nLoad = 200000;
int counter = nWorkers * nLoad;
List<Thread> threads = new List<Thread>();
for (var i = 0; i < nWorkers; i++) {
var th = new Thread((_) => {
for (var j = 0; j < nLoad; j++) {
counter--; // bad
}
});
th.Start();
threads.Add(th);
}
foreach (var w in threads) {
w.Join();
}
counter.Dump();
}
Note that the visibility between threads is of importance. Synchronization guarantees this visibility in addition to atomicity.
This code is easily fixed, at least in the limited context presented. Switch out the decrement and observe the results:
counter--; // bad
Interlocked.Decrement(ref counter); // good
lock (threads) { counter--; } // good
1 Even when using a volatile
variable, the results are still unpredictable. This seems to indicate that (at least here, when I just ran it) it is also not an atomic operator as read/op/write of competing threads were interleaved. To see that the behavior is still incorrect when visibility issues are removed (are they?), add
class x {
public static volatile int counter;
}
and modify the above code to use x.counter
instead of the local counter
variable.