Reputation: 47
I have one class NonVolatileTest :
public class NonVolatileTest
{
public bool _loop = true;
}
and I have two examples of code:
1:
private static void Main(string[] args)
{
NonVolatileTest t = new NonVolatileTest();
Task.Run(() => { t._loop = false; });
while (t._loop) ;
Console.WriteLine("terminated");
Console.ReadLine();
}
2:
private static void Main(string[] args)
{
NonVolatileTest t = new NonVolatileTest();
Task.Run(() => { t._loop = false; });
Task.Run(() =>
{
while (t._loop) ;
Console.WriteLine("terminated");
});
Console.ReadLine();
}
In the first example all works as expected and 'while' cycle is never terminated, but in the second example all works allegedly '_loop' field is volatile.
Why?
PS. VS 2013, .NET 4.5, x64 Release mode & Ctrl + F5
Hypothesis:
This 'bug' may be related to the TaskScheduler. I think, before JIT puts second task for compilation and running, the first task has been finished, so JIT takes the changed value.
Upvotes: 4
Views: 86
Reputation: 47
There is an article about Memory Model: http://igoro.com/archive/volatile-keyword-in-c-memory-model-explained/
There is the table in the part "Memory model and .NET operations" : "table of how various .NET operations interact with the imaginary thread cache."
As I saw, ordinary reading doesn't refresh the thread cache. And I think it means that second task was started after the first task has been finished, because second thread has read 'false' value.
Next code shows result "terminated: 0", as expected in this case.
This part of code is equal 2nd example:
private static void Main(string[] args)
{
NonVolatileTest t = new NonVolatileTest();
Task.Run(() =>
{
var i = 0;
while (t._loop)
{
i++;
}
Console.WriteLine("terminated: {0}", i);
});
//add delay here
Task.Run(() => { t._loop = false; });
Console.ReadLine();
}
This is confirmed by the fact if before starting of the second Task has been added Thread.Sleep(1000) delay, the second task will read the not changed value (true), because the first task is not finished yet, and we have the same behavior as in the 1st example.
Upvotes: -1
Reputation: 391286
According to the C# 5 specification (and the same passage can be found in the annotated C# 4 specification), under section 10.5.3 - Volatile Fields, this is stated:
When a field-declaration includes a volatile modifier, the fields introduced by that declaration are volatile fields. For non-volatile fields, optimization techniques that reorder instructions can lead to unexpected and unpredictable results in multi-threaded programs that access fields without synchronization such as that provided by the lock-statement (§8.12). These optimizations can be performed by the compiler, by the run-time system, or by hardware. For volatile fields, such reordering optimizations are restricted:
(my emphasis)
So this is documented to be unpredictable (aka out of your control).
The fact that the two pieces of code behaves different can come down to the difference between hoisting the code out to a method on the generated object (for the closure) and not hoisting it.
My psychic code reading eyes tells me that this is what is probably happening in the first case:
In the second case, the above scenario is changed slightly by the fact that the first scenario effectively does "read of variable through some object reference" and the second scenario effectively does "read of variable through this
reference", which may impose differences.
But the real answer here is that you're prone to the optimizer and have written unpredictable code.
Don't be alarmed that the result is also unpredictable.
Tiny seemingly unrelated changes to the code can make the optimizer do things differently.
Upvotes: 2