C# compiler optimization and volatile keyword

2020-07-22 15:01发布

问题:

I have read some posts about volatile keyword and behaviour without this keyword.

I've especially tested the code from the answer to Illustrating usage of the volatile keyword in C#. When running, I observe the excepted behaviour in Release mode, without debugger attached. Up to that point, there is no problem.

So, as far as I understand, the following code should never exit.

public class Program
{
    private bool stopThread;

    public void Test()
    {
        while (!stopThread) { }  // Read stopThread which is not marked as volatile
        Console.WriteLine("Stopped.");
    }


    private static void Main()
    {
        Program program = new Program();

        Thread thread = new Thread(program.Test);
        thread.Start();

        Console.WriteLine("Press a key to stop the thread.");
        Console.ReadKey();

        Console.WriteLine("Waiting for thread.");
        program.stopThread = true;

        thread.Join();  // Waits for the thread to stop.
    }
}

Why does it exit? Even in Release mode, without debugger?

Update

An adaptation of the code from Illustrating usage of the volatile keyword in C#.

private bool exit;

public void Test()
{
    Thread.Sleep(500);
    exit = true;
    Console.WriteLine("Exit requested.");
}

private static void Main()
{
    Program program = new Program();

    // Starts the thread
    Thread thread = new Thread(program.Test);
    thread.Start();

    Console.WriteLine("Waiting for thread.");
    while (!program.exit) { }
}

This program does not exit after in Release mode, without debugger attached.

回答1:

So, as far as I understand, the following should never exit.

No, it can stop. It just isn't guaranteed to.

It doesn't stop on the machine I'm currently running on, for example - but equally I could try the exact same executable on another machine and it might behave fine. It will depend on the exact memory model semantics used by the CLR it runs on. That will be affected by the underlying architecture and potentially even the exact CPU being used.

It's important to note that it's not the C# compiler which determines what to do with a volatile field - the C# compiler just indicates the volatility in the metadata using System.Runtime.CompilerServices.IsVolatile. Then the JIT can work out what that means in terms of obeying the relevant contracts.



回答2:

In the comments you said you were targeting a 32 bit x86 architecture. This is important. Also, my answer is going to assume that are already aware that just because the memory model allows something to happen does not mean it always will happen.

Short answer:

It is because the while loop is empty. Of course, many other subtle changes can affect the behavior as well. For example, if you put a call to Console.WriteLine or Thread.MemoryBarrier before the loop then the behavior will change.

Long answer:

There is a difference in the way the 32-bit and 64-bit runtimes are behaving. The 32-bit runtime, for whatever reason, is foregoing the lifting optimization in the absence of an explicit/implicit memory generator preceding the loop or when the while loop itself is empty.

Consider my example from another question on the same subject here. Here it is again below.

class Program
{
    static bool stop = false;

    public static void Main(string[] args)
    {
        var t = new Thread(() =>
        {
            Console.WriteLine("thread begin");
            bool toggle = false;
            while (!stop)
            {
                toggle = !toggle;
            }
            Console.WriteLine("thread end");
        });
        t.Start();
        Thread.Sleep(1000);
        stop = true;
        Console.WriteLine("stop = true");
        Console.WriteLine("waiting...");

        // The Join call should return almost immediately.
        // With volatile it DOES.
        // Without volatile it does NOT.
        t.Join(); 
    }
}

This example does indeed reproduce the "no exit" behavior on 32-bit x86 hardware. Notice how I intentionally have the while loop busy doing something. For whatever reason an empty loop will not reproduce the behavior consistently. Now, let us change your first example using what we learned from above and see what happens.

public class Program
{
    private bool stopThread;

    public void Test()
    {
        bool toggle = true;
        while (!stopThread) // Read stopThread which is not marked as volatile
        { 
          toggle = !toggle;
        }  
        Console.WriteLine("Stopped.");
    }


    private static void Main()
    {
        Program program = new Program();

        Thread thread = new Thread(program.Test);
        thread.Start();

        Console.WriteLine("Press a key to stop the thread.");
        Console.ReadKey();

        Console.WriteLine("Waiting for thread.");
        program.stopThread = true;

        thread.Join();  // Waits for the thread to stop.
    }
}

Using a slightly modified version of your first example to get the while loop doing something you will see that it now starts to exhibit the "no exit" behavior. I just tested this with .NET 4.5 targeting 32-bit x86 on Windows 7 64-bit. I believe you should notice a change in your environment as well. Try it out with the modifications above.