我想编写一个小程序来直观地演示volatile
关键字的行为。理想情况下,这应该是一个并发访问非volatile静态字段的程序,由于这个原因导致了不正确的行为。
在同一程序中添加volatile
关键字应该可以解决这个问题。
但是我尝试了好几次,开启了优化等,总是没有使用"volatile"关键字时得到正确的行为。您有关于这个话题的任何想法吗?您知道如何在简单的演示应用程序中模拟这样的问题吗?这是否取决于硬件?
我已经实现了一个可工作的示例!
主要思路来自维基百科,但对于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");
}
}
是的,这与硬件有关(仅在多处理器情况下才会遇到此问题),但这也与实现有关。 CLR规范中的内存模型规范允许一些Microsoft CLR实现不一定执行的操作。
以下是我对这种行为的贡献...虽然不多,但基于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也可以正常工作,而在另一些实现中可能会失败,因此在C#中很难演示。
如果JIT编译器决定变量的值无法更改,那么相同的情况也可能发生在C#中,因此创建的机器码甚至不再检查它。 如果现在另一个线程正在更改该值,则您的第一个线程仍可能陷入循环。
同样,这也可能发生在C#中,但它强烈依赖于虚拟机和JIT编译器(如果没有JIT,则为解释器……理论上,我认为MS始终使用JIT编译器,Mono也使用其中之一;但您可能能够手动禁用它)。
我发现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
}
}