Alexander Tretyakov
Alexander Tretyakov

Reputation: 47

Memory Model and ThreadPool

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

Answers (2)

Alexander Tretyakov
Alexander Tretyakov

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

Lasse V. Karlsen
Lasse V. Karlsen

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:

  1. The task is spun up, but this has overhead before the actual code in the delegate is called
  2. Before the delegate has been called, the main program continues and manages to start the loop, doing a single read of the control variable, and keep reusing its cached copy.
  3. The delegate is eventually executed, but this has no impact on the loop since it has already read the variable once and has no inclination to do it again.

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

Related Questions