Joseph Albahari 在他关于 C# 线程的出色论文中提出了以下简单程序来演示为什么我们需要在由多个线程读取和写入的数据周围使用某种形式的内存防护。如果您在 Release 模式下编译它并在没有调试器的情况下自由运行它,程序将永远不会结束:
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
以确保它始终与主内存同步。
关于c# - 两个线程之间的共享变量与共享属性的行为不同,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10399628/