Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Threading & implicit memory barriers

Trying to understand .net's memory model when it comes to threading. This question is strictly theoretical and I know it can be resolved in other ways such as using a lock or marking _task as volatile.

Take the following piece of code for example:

class Test
{
    Task _task;
    int _working = 0;

    public void Run()
    {
        if (Interlocked.CompareExchange(ref _working, 1, 0) == 0)
        {
            _task = Task.Factory.StartNew(() =>
            {
                //do some work...
            });
            _task.ContinueWith(antecendent => Interlocked.Exchange(ref _working, 0));
        }
    }

    public void Dispose()
    {
        if (Interlocked.CompareExchange(ref _working, _working, 0) == 1)
        {
            _task.ContinueWith(antecendent => { /*do some other work*/ });
        }
    }
}

Now make the following assumptions:

  1. Run can be called multiple times (from different threads) and will never be called after Dispose has been called.
  2. Dispose will be called exactly once.

Now to my question, will the value of _task (in the Dispose method) always be a "fresh" value, meaning will it be read from the "main memory" as opposed to being read from a register? From what I've been reading Interlocked creates a full fence memory barrier, so I'm assuming _task will be read from main memory or am I completely off?

like image 209
Tsef Avatar asked May 26 '14 14:05

Tsef


1 Answers

Aside from the intricacies of using the phrase "fresh read" too loosely then yes, _task will be reacquired from main memory. However, there may be separate and even more subtle problem with your code. Consider an alternate, but exactly equivalent, structure for your code which should make it easier to spot the potential problem.

public void Dispose()
{
    int register = _working;
    if (Interlocked.CompareExchange(ref _working, register, 0) == 1)
    {
        _task.ContinueWith(antecendent => { /*do some other work*/ });
    }
}

The second parameter of CompareExchange is passed by-value so it could be cached in a register. I am envisioning the following scenario.

  • Thread A calls Run
  • Thread A does something else with _working that causes it to cache it in a register.
  • Thread B completes the task and calls Exchange from the ContinueWith delegate.
  • Thread A calls Dispose.

In the above scenario _working would change to 1 then 0 followed by Dispose flipping it back to a 1 (because that value was cached in a register) without even going into the if statement. At that point _working could be in an inconsistent state.

Personally, I think this scenario is highly unlikely mostly because I do not think _working would get cached in that manner especially if you always made sure to protect accesses to it with interlocked operations.

If nothing else I hope it gives you some food for thought regarding how complicated lock-free techniques can get.

like image 173
Brian Gideon Avatar answered Oct 24 '22 02:10

Brian Gideon