3

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.

Community
  • 1
  • 1
Cédric Bignon
  • 12,892
  • 3
  • 39
  • 51
  • 2
    Because you set stopThread to true... – ta.speot.is Jul 28 '13 at 11:51
  • I don't know if the JITter ever uses a read elision of a field as an optimization. – CodesInChaos Jul 28 '13 at 11:55
  • 2
    @ta.speot.is In many languages the compiler would determine that `stopThread` isn't changes inside the loop and thus is free to assume that it will either loop forever, or never loop at all. In such languages it's not obliged to re-read it on every iteration, and thus might not notice that another thread changed it. I'm not sure about what the CLR/.net guarantees in this situation. – CodesInChaos Jul 28 '13 at 11:56
  • @ta.speot.is I've added at the end of my post an adaptation of http://stackoverflow.com/questions/133270/illustrating-usage-of-the-volatile-keyword-in-c-sharp which does not exit. Even if exit is set to `true`. – Cédric Bignon Jul 28 '13 at 11:57
  • 1
    @CédricBignon Just because a compiler *may* perform such an optimization doesn't mean that it *must*. So it's not too surprising if the behavior isn't consistent. – CodesInChaos Jul 28 '13 at 11:58
  • As Jon and others correctly point out, that an optimization is *legal* does not make it *required*. Moreover: you are probably testing by running code on a chip with a strong memory model, like x86. On x86 every write is essentially a volatile write. If you want to try to produce examples of memory model bugs, try running your code on weak memory model hardware, like ARM devices. – Eric Lippert Jul 28 '13 at 14:25
  • Could you describe your environment? My experience with doing these kinds of experiments seems to indicate that the "not exiting" behavior should be observed nearly 100% of the time on either an x86 or x64 running .NET 2.0 or greater and ran in Release build outside of the debugger. In fact, I just now copied your code exactly and ran it on x64 and .NET 4.5 and it did not exit as expected. So what is different with your environment? – Brian Gideon Jul 28 '13 at 18:29
  • @BrianGideon I'm on VS 2012, Windows 8, x86, Release, without debugger with Intel i7. – Cédric Bignon Jul 28 '13 at 18:31
  • @CédricBignon: Yeah...okay. When I changed the build to target x86 (even though I'm running on Windows 7 64-bit) in Visual Studio 2012 it starts exiting. So that's it. I didn't realize that's what it was. This behavior may have changed recently as I'm almost certain I've been able to reproduce it on 32-bit machines quite consistently in the past. – Brian Gideon Jul 28 '13 at 18:37

2 Answers2

10

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.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • While it's clearly allowed to do so, it's just a bit weird that the JITter chose to treat those two samples differently. – CodesInChaos Jul 28 '13 at 12:03
  • @CodesInChaos: That's far from the weirdest thing I've seen in terms of JIT behaviour. Basically if you step outside the bounds of guaranteed behaviour, you can encounter all kinds of weirdnesses. – Jon Skeet Jul 28 '13 at 12:05
  • @CodesInChaos: It is strange. After a bit of experimentation I discovered it is the placement of the `Console.WriteLine` call that is making a difference in this case. Note, in the former it is *after* the loop, but in the later it is *before*. That is the key difference in these examples. – Brian Gideon Jul 28 '13 at 20:09
  • @BrianGideon: That actually makes complete sense: the JIT compiler doesn't know what will happen in `Console.WriteLine`, so it can't perform as much optimization. For example, for all it knows that code could end up writing to `stopThread` somehow, at which point the loop *would* have to finish. – Jon Skeet Jul 28 '13 at 20:15
  • @JonSkeet: Yeah, I thought the same thing at first. But, by that logic you would think the first example would hang and the second example would exit. The exact opposite happens though as the OP pointed out. That's why I said it was strange. Then again, I'm getting distracted right now because my wife is "making" me sample some cookies that she is baking so I might not be giving this problem the attention it deserves :) – Brian Gideon Jul 28 '13 at 20:34
  • @BrianGideon: Hmmm... it's the first example which hangs for me. But the second example in the question doesn't have `Console.WriteLine` *in* the loop which is the important bit - I'd misread your comment before. I believe that with a call within the loop, it will *always* exit. – Jon Skeet Jul 28 '13 at 20:37
1

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.

Community
  • 1
  • 1
Brian Gideon
  • 47,849
  • 13
  • 107
  • 150