在他关于C#中线程的优秀论文中,Joseph Albahari提出了以下简单程序来演示为什么我们需要对多个线程读取和写入的数据使用某种形式的内存屏障.如果您在发布模式下编译它并在没有调试器的情况下自由运行它,程序永远不会结束:
static void Main() { bool complete = false; var t = new Thread(() => { bool toggle = false; while (!complete) toggle = !toggle; }); t.Start(); Thread.Sleep(1000); complete = true; t.Join(); // Blocks indefinitely }
我的问题是,为什么以上稍微修改过的上述程序版本不再无限期地阻塞?
class Foo { public bool Complete { get; set; } } class Program { static void Main() { var foo = new Foo(); var t = new Thread(() => { bool toggle = false; while (!foo.Complete) toggle = !toggle; }); t.Start(); Thread.Sleep(1000); foo.Complete = true; t.Join(); // No longer blocks indefinitely!!! } }
以下仍然无限期地阻止:
class Foo { public bool Complete;// { get; set; } } class Program { static void Main() { var foo = new Foo(); var t = new Thread(() => { bool toggle = false; while (!foo.Complete) toggle = !toggle; }); t.Start(); Thread.Sleep(1000); foo.Complete = true; t.Join(); // Still blocks indefinitely!!! } }
如下所示:
class Program { static bool Complete { get; set; } static void Main() { var t = new Thread(() => { bool toggle = false; while (!Complete) toggle = !toggle; }); t.Start(); Thread.Sleep(1000); Complete = true; t.Join(); // Still blocks indefinitely!!! } }
解决方法
在第一个示例中,Complete是一个成员变量,可以在每个线程的寄存器中进行缓存.由于您没有使用锁定,因此可能无法将对该变量的更新刷新到主存储器,而另一个线程将看到该变量的过时值.
在第二个示例中,Complete是一个属性,实际上是在Foo对象上调用一个函数来返回一个值.我的猜测是,虽然简单变量可以缓存在寄存器中,但编译器可能并不总是以这种方式优化实际属性.
编辑:
关于自动属性的优化 – 我不认为规范在这方面有任何保证.您实质上是在考虑编译器/运行时是否能够优化getter / setter.
如果它在同一个对象上,它似乎就是这样.在另一种情况下,它似乎没有.无论哪种方式,我都不会赌它.解决这个问题的最简单方法是使用一个简单的成员变量,标记为volotile,以确保它始终与主内存同步.