今天在看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种:

  • 变量会被其他线程更改
  • 变量会被硬件更改

    第一种比较好理解,第二种对于写应用程序级别的人来说,比较难理解,也不太会遇到,就死记硬背好了,呵呵。

posted on 2010-05-07 09:37  Zhenway  阅读(675)  评论(0编辑  收藏  举报