C#编译器优化和volatile关键字

3

我已经阅读了一些关于volatile关键字和没有使用该关键字时的行为的帖子。

我特别测试了在C#中说明使用volatile关键字的用法答案中的代码。当运行时,在Release模式下,没有调试器连接时,我观察到了期望的行为。到那个时候,没有问题。

因此,就我所理解的而言,以下代码应该永远不会退出。

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.
    }
}

为什么它会出现?即使在没有调试器的情况下,也是如此。

更新

说明C#中使用volatile关键字的用法的代码进行了适应。

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) { }
}

在Release模式下,没有调试器附加时,该程序不会退出。


2
因为您将stopThread设置为true... - ta.speot.is
2
在许多编程语言中,编译器会确定stopThread在循环内部没有被更改,因此可以自由地假设它要么永远循环,要么根本不循环。在这种语言中,编译器不必在每次迭代中重新读取它,因此可能不会注意到另一个线程已经更改了它。我不确定CLR/.net在这种情况下的保证。 - CodesInChaos
1
@CédricBignon 编译器可能执行这样的优化,并不意味着它必须这样做。因此,如果行为不一致,也就不足为奇了。 - CodesInChaos
你能描述一下你的环境吗?我的经验表明,在 x86 或 x64 上运行 .NET 2.0 或更高版本,并在 Release 模式下在调试器外运行时,几乎总是会观察到“不退出”的行为。事实上,我刚刚完全复制了你的代码,并在 x64 和 .NET 4.5 上运行它,但它并没有像预期的那样退出。那么你的环境有什么不同呢? - Brian Gideon
@BrianGideon 我在使用VS 2012,Windows 8,x86,Release版本,在Intel i7上没有使用调试器。 - Cédric Bignon
显示剩余4条评论
2个回答

10

据我理解,以下内容永远不应该退出。

不,它是可以停止的。只是不能保证会停止。

例如,在我当前运行的计算机上它不会停止 - 但同样地,我可以在另一台计算机上尝试完全相同的可执行文件,它可能表现正常。这将取决于它所运行的CLR使用的确切内存模型语义。这将受到底层架构的影响,甚至可能受到使用的确切CPU的影响。

需要注意的是,它不是由来确定如何处理volatile字段 - C#编译器只是使用System.Runtime.CompilerServices.IsVolatile在元数据中指示了易失性。然后可以通过遵守相关合同来确定其意义。


虽然这样做显然是允许的,但JIT编译器选择以不同的方式处理这两个示例有点奇怪。 - CodesInChaos
@CodesInChaos:就JIT行为而言,我见过的远不止这种怪异的事情。基本上,如果你超出了保证行为的范围,你可能会遇到各种奇怪的问题。 - Jon Skeet
@CodesInChaos: 这很奇怪。经过一些尝试,我发现在这种情况下Console.WriteLine调用的位置是有影响的。请注意,在前一个示例中,它在循环之后,但在后一个示例中则是在循环之前。这就是这些示例之间的关键区别。 - Brian Gideon
@BrianGideon:这其实是完全有道理的:JIT编译器不知道在Console.WriteLine中会发生什么,因此它无法进行太多的优化。例如,对于所有它所知道的代码来说,它最终可能会写入stopThread,此时循环必须完成。 - Jon Skeet
@JonSkeet:是的,我一开始也这么想。但是按照这个逻辑,你会认为第一个例子会挂起,而第二个例子会退出。然而正好相反,正如OP所指出的那样。这就是为什么我说这很奇怪。不过,现在我有些分心了,因为我的妻子正在“强迫”我品尝她烤的一些饼干,所以我可能没有给这个问题应有的关注 :) - Brian Gideon
@BrianGideon:嗯......对我而言,第一个示例是挂起的。但是问题中的第二个示例在循环中没有Console.WriteLine,这是重要的部分——之前我误读了您的评论。我认为,在循环内调用它时,它将始终退出。 - Jon Skeet

1
在评论中,您说您的目标是32位x86架构。这很重要。此外,我的答案会假设您已经意识到,仅因为内存模型“允许”某些事情发生,并不意味着它总是会发生。
简短回答:
这是因为while循环是空的。当然,许多其他微妙的更改也会影响行为。例如,如果在循环之前调用Console.WriteLine或Thread.MemoryBarrier,则行为将发生变化。
长回答:
32位和64位运行时的行为有所不同。由于某种原因,32位运行时在没有显式/隐式内存生成器前置循环或while循环本身为空的情况下放弃提升优化。
考虑我在另一个关于相同主题的问题中的示例here。下面是它。
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(); 
    }
}

这个例子确实在32位x86硬件上重现了“无法退出”的行为。请注意,我故意让while循环忙于做某些事情。由于某种原因,一个空循环不会始终重现该行为。现在,让我们使用上面学到的知识来改变你的第一个例子,看看会发生什么。
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.
    }
}

使用稍微修改过的第一个示例来让while循环执行一些操作,你会发现它现在开始表现出“无法退出”的行为。我刚刚在Windows 7 64位上测试了.NET 4.5针对32位x86的目标。我相信你也应该注意到你的环境有所改变。使用以上修改进行尝试。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接