我想编写一个小程序,直观地说明volatile
关键字的行为。理想情况下,它应该是一个对非易失性静态字段执行并发访问并因此获得不正确行为的程序。
在同一个程序中添加 volatile 关键字应该可以解决问题。
那是我没能做到的。即使尝试了几次,启用优化等,我总是能在没有“volatile”关键字的情况下得到正确的行为。
你对这个话题有什么想法吗?你知道如何在一个简单的演示应用程序中模拟这样的问题吗?它取决于硬件吗?
我已经实现了一个工作示例!
主要思想来自 wiki,但对 C# 进行了一些更改。wiki文章为C++的静态字段演示了这一点,看起来C#总是仔细地编译对静态字段的请求......我用非静态的做例子:
如果您在Release模式下运行此示例并且没有调试器(即使用 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");
}
}
是的,它取决于硬件(如果没有多个处理器,您不太可能看到问题),但它也取决于实现。CLR 规范中的内存模型规范允许 CLR 的 Microsoft 实现不一定要做的事情。
当没有指定 'volatile' 关键字时,这并不是真正的故障问题,更多的是在没有指定时可能会发生错误。通常,您会比编译器更好地知道这种情况!
考虑它的最简单方法是编译器可以,如果它愿意,内联某些值。通过将值标记为 volatile,您是在告诉自己和编译器该值实际上可能会发生变化(即使编译器不这么认为)。这意味着编译器不应内联值、保留缓存或提前读取值(以尝试优化)。
这种行为实际上与 C++ 中的关键字不同。
MSDN在这里有一个简短的描述。这是一篇关于易失性、原子性和联锁主题的更深入的帖子
在 C# 中很难演示,因为代码是由虚拟机抽象出来的,因此在这台机器的一个实现上,它可以在没有 volatile 的情况下正常工作,而在另一个实现上可能会失败。
不过,Wikipedia 有一个很好的例子,如何用 C 来演示它。
如果 JIT 编译器决定变量的值无论如何都不能改变,因此在 C# 中可能会发生同样的事情,从而创建甚至不再检查它的机器代码。如果现在另一个线程正在更改该值,则您的第一个线程可能仍会陷入循环。
同样,这也可能发生在 C# 上,但它在很大程度上取决于虚拟机和 JIT 编译器(或解释器,如果它没有 JIT ......理论上,我认为 MS 总是使用 JIT 编译器,Mono 也使用一个;但您可以手动禁用它)。
这是我对这种行为的集体理解的贡献......这并不多,只是一个演示(基于 xkip 的演示),它显示了易失性与非易失性(即“正常”)int值的行为,并排-side,在同一个程序中......这就是我找到这个线程时正在寻找的东西。
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行为不端的地方正确运行。
我已经尝试过并且失败了这个挑战。(实际上我很快就放弃了;-)。在我尝试使用静态变量的所有内容中,无论它们是否易变,它们的行为都“正确”......我很想解释为什么会这样,如果确实如此......是这样吗?编译器不会在寄存器中缓存静态变量的值(即,它会缓存对该堆地址的引用)?
不,这不是一个新问题......这是试图让社区回到原来的问题。
我遇到了乔·阿尔巴哈里(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
}
}