在C#中说明关键字volatile的用法

89

我想编写一个小程序来直观地演示volatile关键字的行为。理想情况下,这应该是一个并发访问非volatile静态字段的程序,由于这个原因导致了不正确的行为。

在同一程序中添加volatile关键字应该可以解决这个问题。

但是我尝试了好几次,开启了优化等,总是没有使用"volatile"关键字时得到正确的行为。您有关于这个话题的任何想法吗?您知道如何在简单的演示应用程序中模拟这样的问题吗?这是否取决于硬件?

6个回答

105

我已经实现了一个可工作的示例!

主要思路来自维基百科,但对于C#进行了一些更改。维基文章演示了C++的静态字段,看起来C#总是会仔细编译对静态字段的请求... 我用非静态的字段做了一个示例:

如果您在Release模式下并且没有调试器(即使用Ctrl+F5)运行此示例,则while (test.foo != 255)这一行将被优化为“while(true)”,并且该程序永远不会返回。 但是添加volatile关键字后,您将始终获得“OK”。

class Test
{
    /*volatile*/ int foo;

    static void Main()
    {
        var test = new Test();

        new Thread(delegate() { Thread.Sleep(500); test.foo = 255; }).Start();

        while (test.foo != 255) ;
        Console.WriteLine("OK");
    }
}

5
经过测试,使用.NET 4.0的x86和x64版本 - 可以确认该示例仍然适用。谢谢! :) - Roman Starkov
1
太好了!我从来不知道JIT会进行这种优化,而volatile则会禁用它。 - usr
3
请明确一下,您是在foo的声明中加入了"volatile"关键字,对吗? - JoeCool
例子起作用了。我确信我遇到的问题(线程卡在循环中)与易变性(或缺乏易变性)有关,但即使人为地创建了这种情况,也无法重现,直到我打开优化。将“易失性”放在布尔值上修复了无限循环问题,但现在有一个关于代码中其他地方是否这样做的担忧,因此决定关闭优化。我对此感到有些惊讶,或者说只是如何打开优化(这是最近才完成的)可能会咬一口。 - TByte

21

是的,这与硬件有关(仅在多处理器情况下才会遇到此问题),但这也与实现有关。 CLR规范中的内存模型规范允许一些Microsoft CLR实现不一定执行的操作。


6
这并不是说没有使用'volatile'关键字就会出现故障,更多的是在未指定时可能会出现错误。一般来说,你比编译器更清楚这种情况!
最简单的想法是,如果愿意的话,编译器可以内联某些值。通过将值标记为volatile,你告诉自己和编译器该值实际上可能会发生变化(即使编译器认为不会)。这意味着编译器不应内联值,保留缓存或早期读取值(以尝试进行优化)。
这种行为与C++中的关键字并不完全相同。
MSDN有一个简短的描述here。 这里有一个更深入的帖子,涉及到 Volatility, Atomicity and Interlocking

4

以下是我对这种行为的贡献...虽然不多,但基于xkip的演示,它展示了一个易失性和一个非易失性(即“正常”)int值在同一程序中的行为...这正是我在找到这个线程时想要的。

using System;
using System.Threading;

namespace VolatileTest
{
  class VolatileTest 
  {
    private volatile int _volatileInt;
    public void Run() {
      new Thread(delegate() { Thread.Sleep(500); _volatileInt = 1; }).Start();
      while ( _volatileInt != 1 ) 
        ; // Do nothing
      Console.WriteLine("_volatileInt="+_volatileInt);
    }
  }

  class NormalTest 
  {
    private int _normalInt;
    public void Run() {
      new Thread(delegate() { Thread.Sleep(500); _normalInt = 1; }).Start();
      // NOTE: Program hangs here in Release mode only (not Debug mode).
      // See: https://dev59.com/OXVC5IYBdhLWcg3w9GPM
      // for an explanation of why. The short answer is because the
      // compiler optimisation caches _normalInt on a register, so
      // it never re-reads the value of the _normalInt variable, so
      // it never sees the modified value. Ergo: while ( true )!!!!
      while ( _normalInt != 1 ) 
        ; // Do nothing
      Console.WriteLine("_normalInt="+_normalInt);
    }
  }

  class Program
  {
    static void Main() {
#if DEBUG
      Console.WriteLine("You must run this program in Release mode to reproduce the problem!");
#endif
      new VolatileTest().Run();
      Console.WriteLine("This program will now hang!");
      new NormalTest().Run();
    }

  }
}

上面有一些非常精简的解释,以及一些很好的参考资料。感谢所有人帮我理解volatile(至少让我知道不要在我的第一个本能选择是lock的情况下依赖于volatile)。
干杯,感谢所有的鱼。Keith。
PS:我很想看到原始请求的演示,即:“我想看到静态易失性int表现正确的地方,其中静态int表现不佳。”
我尝试并失败了这个挑战。(实际上我很快放弃了;-)。在我尝试的所有静态变量中,无论它们是否易失性,它们都表现得“正确”,如果确实是这种情况,我希望解释一下…是编译器不会将静态变量的值缓存到寄存器中吗?(即,它缓存指向该堆地址的引用)。
不,这不是一个新问题…而是试图将社区引导回原来的问题。

4

由于代码被虚拟机抽象化,因此在某些实现中,即使没有使用volatile也可以正常工作,而在另一些实现中可能会失败,因此在C#中很难演示。

维基百科有一个很好的例子来演示它在C中如何实现。

如果JIT编译器决定变量的值无法更改,那么相同的情况也可能发生在C#中,因此创建的机器码甚至不再检查它。 如果现在另一个线程正在更改该值,则您的第一个线程仍可能陷入循环。

另一个例子是忙等待。

同样,这也可能发生在C#中,但它强烈依赖于虚拟机和JIT编译器(如果没有JIT,则为解释器……理论上,我认为MS始终使用JIT编译器,Mono也使用其中之一;但您可能能够手动禁用它)。


2

我发现Joe Albahari的以下文字对我非常有帮助。

我从上述文本中提取了一个例子,并创建了一个静态易变字段。当您删除volatile关键字时,程序将无限期地阻塞。在发布模式下运行此示例。

class Program
{
    public static volatile bool complete = false;

    private static void Main()
    {           
        var t = new Thread(() =>
        {
            bool toggle = false;
            while (!complete) toggle = !toggle;
        });

        t.Start();
        Thread.Sleep(1000); //let the other thread spin up
        complete = true;
        t.Join(); // Blocks indefinitely when you remove volatile
    }
}

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