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


88

我想编写一个小程序,以可视方式说明volatile关键字的行为。理想情况下,它应该是对非易失性静态字段执行并发访问并因此而获得错误行为的程序。

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

那是我无法实现的。即使尝试几次,启用优化等,我也总是得到正确的行为而没有'volatile'关键字。

您对此主题有任何想法吗?您知道如何在简单的演示应用程序中模拟此类问题吗?它取决于硬件吗?

Answers:


102

我已经取得了一个可行的例子!

主要思想来自Wiki,但对C#进行了一些更改。Wiki文章针对C ++的静态字段演示了这一点,似乎C#总是仔细地将请求编译为静态字段...而我以非静态示例为例:

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

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 2012年

3
明确地说,您要在foo的声明中添加“ volatile”关键字,对吗?
JoeCool 2012年

21

是的,它取决于硬件(如果没有多个处理器,您不太可能看到问题),但它也取决于实现。CLR规范中的内存模型规范允许Microsoft实施CLR不一定要做的事情。


6

当未指定'volatile'关键字时,发生错误并不是真正的问题,更多的是未指定关键字时可能发生错误。通常,您会知道什么时候比编译器更好!

最简单的思考方法是,如果愿意,编译器可以内联某些值。通过将值标记为易失性,就可以告诉自己和编译器该值实际上可能会发生变化(即使编译器认为不是这样)。这意味着编译器不应内联值,保持高速缓存或尽早读取值(以进行优化)。

这种行为与C ++中的关键字并不完全相同。

MSDN 在这里有简短描述。这可能是有关波动率,原子性和联锁性的更深入的文章


4

很难用C#演示,因为代码是由虚拟机抽象的,因此,在该计算机的一种实现上,它可以正常工作而不会不稳定,而在另一种实现上可能会失败。

维基百科有一个很好的例子,说明了如何用C语言进行演示。

如果JIT编译器确定该变量的值无论如何都无法更改,从而创建了甚至不再检查它的机器代码,则在C#中可能发生相同的事情。如果现在另一个线程正在更改该值,则您的第一个线程可能仍会陷入循环。

另一个例子是忙等待。

同样,这也可能在C#中发生,但是在很大程度上取决于虚拟机和JIT编译器(或解释器,如果它没有JIT ...从理论上讲,我认为MS始终使用JIT编译器,Mono也使用一个;但是您可以手动将其禁用)。


4

这是我对这种行为的集体理解的贡献...数量不多,只是一个演示(基于xkip的演示),它展示了volatile诗句与非易失性(即“正常”)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: http://stackoverflow.com/questions/133270/illustrating-usage-of-the-volatile-keyword-in-c-sharp
      // 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(至少知道不依赖volatile我的第一个本能所在)lock)。

干杯,感谢所有的鱼。基思


PS:我对原始请求的演示非常感兴趣,该演示是:“我希望看到一个静态 volatile int行为正确,而一个静态 int行为不。

我已经尝试并失败了这一挑战。(实际上我很快就放弃了;-)。在我使用静态var进行的所有尝试中,无论它们是否易失,它们的行为都“正确” ……而且我很喜欢这种情况的解释,如果确实如此的话。编译器没有在寄存器中缓存静态vars 的(即,它缓存对该堆地址的引用)?

不,这不是一个新问题……这是试图使社区回到最初的问题。


2

我遇到了乔·阿尔巴哈里(Joe Albahari)的以下经文,对我有很大帮助。

我从上面的文本中获取了一个示例,通过创建一个静态的volatile字段,对其进行了一些更改。当您删除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
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.