8

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?

Kristof U.
  • 1,263
  • 10
  • 17
Tsef
  • 1,018
  • 9
  • 22
  • There's little reason to assume that any code is modifying the _task variable after it got started. Variables that are only ever read cannot have the wrong value. The big advantage of using the *lock* statement is that it is much easier to reason through. You would have had some odds to avoid the undebuggable threading race bug you put in your code. You have no guarantee whatsoever that the task actually will continue to the added task. – Hans Passant May 26 '14 at 16:34
  • Reading your answer I assume you didn't really read my question. First of all the _task variable can change if Run was called once again (take a look at the continuation in the Run method). I am well aware that using a lock would have been easier to debug, read etc. and I wouldn't use code such as the above in a real scenario. However this is just an example for something I'm trying to dig deeper into which is the whole memory barrier model. – Tsef May 26 '14 at 17:46
  • I would not call that ".NET's memory model". Barriers go down to assembler instructions - cache synchronicity across core caches is not exactly a .NET problem. – TomTom May 29 '14 at 19:23
  • @TomTom The C# language makes certain guarantees about how given variables will behave, which you can call the languages memory model. As long as, according to the rules of the language's specs, the code should work, then you don't really need to care about the details of how the C# language enforces the constraints of its memory model. That is, unless you find a bug in the C# language, but that's generally not something you expect to run into very often. – Servy May 29 '14 at 19:25
  • From "Threading in C#" by Joe Albahari: "All of Interlocked’s methods generate a full fence. Therefore, fields that you access via Interlocked don’t need additional fences — unless they’re accessed in other places in your program without Interlocked or a lock." http://www.albahari.com/threading/part4.aspx#_Nonblocking_Synchronization – Kris Vandermotten May 29 '14 at 19:45
  • I've also read that book and I understand that, but _task was not accessed via interlocked. – Tsef Jun 01 '14 at 07:32

2 Answers2

1

I do not code in C#, but if full memory barrier is employed, then your interpretation is correct. The compiler should not re-use a value stored in registers, but rather fetch it in a way that ensures memory ordering barriers are not masking the actual value present in the memory subsystem.

I have also found this answer that clearly explains that this is in fact the case, so the documentation you have read seems to be correct: Does Interlocked.CompareExchange use a memory barrier?

Community
  • 1
  • 1
Alexandros
  • 2,097
  • 20
  • 27
1

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.

Brian Gideon
  • 47,849
  • 13
  • 107
  • 150
  • I see your point and completely agree with you. I wouldn't use such code in a real world scenario unless I absolutely had too since it makes it a bit harder to understand/read. What I wanted was to understand the case better which I think I have. Thanks for your input. – Tsef Jun 01 '14 at 07:36
  • @zaf: I'm with you. I don't use lock-free much either. But, there is a lot of benefit in understanding how everything works behind the scenes anyway. I still have a lot to learn myself. – Brian Gideon Jun 01 '14 at 15:43