今天在看social.msdn.microsoft.com论坛上面有个帖子提出了这个问题:
The following peice of code is from C# 4.0 in a Nutshell. Ideally/logically this program should finish within 2-3 seconds, but it seems be caught in an infinite loop. Can someone explain why this happens and how one knows in advance that such a thing will happen for someother code that he/she has written. And who do you think is at fault here OS or .NET?(原始连接)
其有疑问题的代码为:
static void Main() { bool complete = false; var t = new Thread(() => { //Console.WriteLine("in thread"); bool toggle = false; while (!complete) { //Console.WriteLine("in loop"); //Console.WriteLine(complete); toggle = !toggle; } }); Console.WriteLine("starting thread"); t.Start(); Console.WriteLine("thread start"); Thread.Sleep(1000); Console.WriteLine("setting complete"); complete = true; Console.WriteLine("complete set"); Console.WriteLine("Complete"+ complete); t.Join(); // Blocks indefinitely Console.WriteLine("done"); }
其中,代码是运行在4.0上的,根据代码的表面意思,1秒后complete的值会修改为true,然后导致循环的条件不满足,从而退出线程,而实际的结果却是线程无法退出,为什么哪?
这段代码的表面意思虽然没错,但是,在CLR4.0的优化下(CLR 2.0的优化还没有如此强悍),有些隐藏意思被翻译出来,阻止了线程的退出,准确的说,是阻止了线程中循环条件变成false。
在CLR看来,线程中有2个内存地址,一个是complete,另一个是toggle。其中对toggle有读取操作,也有写入操作,而对complete而言,只有读取操作。
并且,由于没有给CLR任何Hint,CLR会认为这个complete不会被其它线程更改,因此,优化为使用寄存器来保存complete的值,除了第一次以外,以后就直接读取寄存器。
这是一个很隐蔽的隐藏含义,但是这个隐藏含义却足以导致整个行为的改变。任何程序都无法修改另一个线程中的某个寄存器的值,因此,线程中的那个被优化了的complete永远为false,也就是最终的结果——线程无法退出。
原楼主问到这个是错误是谁的错误,OS的还是.net的,这里不得不说,这个错误是写应用程序的人与写编译器的人的理解不一致导致的。
写编译器的人会认为,凡是多线程修改的值,写应用程序的人都会给编译器一个Hint,从而只对有Hint的变量不使用优化;而写应用程序的人,却认为所有的变量都不会优化,于是悲剧就发生了。
既然悲剧已经发生了,就要像办法阻止悲剧再次发生,那么这个编译器需要的Hint到底是什么?——volatile关键字(或内存同步机制,lock就是一种内存同步机制,但是不太可能仅仅为了读变量而去lock)
volatile的直译是易变的,有点难理解是不是,举个例子吧:
如果把某个变量声明为volatile的变量,那么,编译器(JIT)就知道这个变量是易变的,所以,任何读写操作都不能优化到寄存器中,必须读写内存地址。
这样,就可以阻止悲剧发生了。
什么时候用volatile?
简单的说,变量会在线程外被更改的情况下,又没有使用lock或其他内存同步机制(Thread.MemoryBarrier等),就需要这个关键字帮忙了。
其中,在线程外被更改的情况可以分为2种:
- 变量会被其他线程更改
- 变量会被硬件更改
第一种比较好理解,第二种对于写应用程序级别的人来说,比较难理解,也不太会遇到,就死记硬背好了,呵呵。