Search code examples
c#multithreadingthread-safetyshared-memorymemory-barriers

Do concurrent interlocked and reads require a memory barrier or locking?


This is a simple problem, but after reading Why do I need a memory barrier? I'm very confused about it.

In the example below, assume different threads are repeatedly calling Increment and Counter:

class Foo{
    int _counter=0;
    public int Counter 
    {
        get { return _counter; }
    }

    public void Increment()
    {
        Interlocked.Increment(ref _counter);
    }
}

Sorry if I'm misinterpreting Why do I need a memory barrier? but it seems like it's suggesting the class above might not be providing a freshness guarantee when reading the value of _counter. Could a thread that's repeatedly accessing the Counter property get forever stuck on an old value of Counter (because it is cached in the register)?

Is a memory barrier or a lock before return _counter; necessary?


Solution

  • Is a memory barrier or a lock before return _counter; necessary?

    Yes, absolutely. Consider the following code.

    while (foo.Counter == 0)
    {
      // Do something
    }
    

    The problem is that if the contents inside the loop are simple enough then the C# compiler, JIT compiler, or hardware will optimize the code in this manner.

    int register = foo._counter;
    while (register == 0)
    {
      // Do something
    }
    

    Or even this.

    if (foo._counter == 0)
    {
      START: 
      // Do something
      goto START;
    }
    

    Notice that I use _counter instead of Counter as a way of implying that the property will probably be inlined. Then, more importantly, the JIT compiler may "lift" the read of _counter outside of the loop so that it is only read once.

    Memory barriers do not provide a freshness guarantee per se. What they do is prevent certain software or hardware optimizations that reorder reads and writes to memory. The freshness guarantee is more of a side effect really.

    So to wrap things up your Counter property should look like this.

    public int Counter 
    {
        get { return Interlocked.CompareExchange(ref _counter, 0, 0); }
    }