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:
Run
can be called multiple times (from different threads) and will never be called after Dispose
has been called. 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?
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.
Run
_working
that causes it to cache it in a register. Exchange
from the ContinueWith
delegate. 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With