Search code examples
c#multithreadingthread-safety

How do memory barriers achieve thread-safety?


In C#10 in a Nutshell, Joseph Albahari writes:

A memory barrier is a “fence” through which the effects of reordering and caching cannot penetrate.

This applies not just to locks but to all synchronization constructs. So, if your use of a signaling construct, for instance, ensures that just one thread reads/writes a variable at a time, you don’t need to lock. Hence, the following code is thread-safe without locking around x

while giving this example:

var signal = new ManualResetEvent (false);
int x = 0;
new Thread (() => { x++; signal.Set(); }).Start();
signal.WaitOne();
Console.WriteLine (x); // 1 (always)

x++ isn't supposed to be thread-safe (this question). What I get from the above quote is that x++ is made thread-safe by doing this implicitly in code:

Thread.MemoryBarrier();
x++;
Thread.MemoryBarrier();

My questions is:

  1. How does a memory barrier really achieve thread-safety by just preventing reordering and caching?

  2. Where exactly would all the implicit MemoryBarriers() be placed in the code sample?


Solution

  • How does a memory barrier really achieve thread-safety by just preventing reordering and caching?

    In Joseph Albahari's example, the implicit memory barriers don't achieve thread-safety by themselves. They are part of a greater mechanism, that also includes signaling. Remove either the barriers or the signaling, and you are left with a buggy mechanism. In order to get the desirable thread-safety, both components should coexist. The signaling ensures that one thread will wait until the other sends the signal that is has done incrementing the x. The memory barrier ensures that the compiler/Jitter/processor will not try to optimize the operation by reordering instructions and moving them from one side of the barrier to the other. Signaling is about threads, and barriers are about memory access.

    Where exactly would all the implicit MemoryBarriers() be placed in the code sample?

    There are two barriers.

    • One is placed before the signal.Set();, to prevent the compiler/Jitter/processor from reordering the x++ instructions, and moving them after the signal.Set();.
    • Another one is placed after the signal.WaitOne();, to prevent the compiler/Jitter/processor from reordering the reading of x (in the Console.WriteLine(x);), and moving it before the signal.WaitOne();.