回归null
不是问题。问题是新实例可能处于另一个线程感知的部分构造状态。考虑这个声明Foo
。
class Foo
{
public int variable1;
public int variable2;
public Foo()
{
variable1 = 1;
variable2 = 2;
}
}
以下是 C# 编译器、JIT 编译器或硬件如何优化代码。1
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
instance = alloc Foo;
instance.variable1 = 1; // inlined ctor
instance.variable2 = 2; // inlined ctor
}
}
}
return instance;
首先,请注意构造函数是内联的(因为它很简单)。现在,希望很容易看到instance
在其组成字段在构造函数中初始化之前被分配了引用。这是一个有效的策略,因为读写可以自由地上下浮动,只要它们不越过边界lock
或改变逻辑流;他们没有。所以另一个线程可以在它完全初始化之前看到instance != null
并尝试使用它。
volatile
修复了这个问题,因为它将读取视为获取栅栏并将写入视为释放栅栏。
- 获取栅栏:不允许其他读写在栅栏之前移动的内存屏障。
- release-fence:一个内存屏障,不允许其他读写在栅栏之后移动。
因此,如果我们标记instance
为,volatile
则释放栅栏将阻止上述优化。以下是带有屏障注释的代码的外观。我使用 ↑ 箭头表示释放栅栏,使用 ↓ 箭头表示获取栅栏。请注意,不允许任何东西向下飘过 ↑ 箭头或向上飘过 ↓ 箭头。把箭头想象成把一切都推开。
var local = instance;
↓ // volatile read barrier
if (local == null)
{
var lockread = padlock;
↑ // lock full barrier
lock (lockread)
↓ // lock full barrier
{
local = instance;
↓ // volatile read barrier
if (local == null)
{
var ref = alloc Foo;
ref.variable1 = 1; // inlined ctor
ref.variable2 = 2; // inlined ctor
↑ // volatile write barrier
instance = ref;
}
↑ // lock full barrier
}
↓ // lock full barrier
}
local = instance;
↓ // volatile read barrier
return local;
对 的 组成变量的写入Foo
仍然可以重新排序,但请注意,内存屏障现在阻止它们在分配给 之后发生instance
。使用箭头作为指导,想象允许和不允许的各种不同的优化策略。请记住,不允许读取或写入通过 ↑ 箭头或向上通过 ↓ 箭头。
Thread.VolatileWrite
也可以解决这个问题,并且可以在没有volatile
关键字的语言中使用,如 VB.NET。如果你看看是如何VolatileWrite
实现的,你会看到这一点。
public static void VolatileWrite(ref object address, object value)
{
Thread.MemoryBarrier();
address = value;
}
现在这乍一看似乎违反直觉。毕竟,内存屏障是在赋值之前放置的。将任务提交到您要求的主存储器怎么样?在分配之后放置障碍不是更正确吗?如果这是你的直觉告诉你的,那就错了。您会看到内存屏障并不是严格意义上的“新读取”或“提交写入”。这都是关于指令排序的。这是迄今为止我看到的最大的混乱来源。
可能还需要提到的是,它Thread.MemoryBarrier
实际上会产生一个完整的屏障。因此,如果我将上面的符号与箭头一起使用,那么它看起来像这样。
public static void VolatileWrite(ref object address, object value)
{
↑ // full barrier
↓ // full barrier
address = value;
}
因此,从技术上讲,调用VolatileWrite
比写入volatile
字段所做的更多。请记住,这volatile
在 VB.NET 中是不允许的,但VolatileWrite
它是 BCL 的一部分,因此可以在其他语言中使用。
1这种优化主要是理论上的。ECMA 规范在技术上确实允许这样做,但 ECMA 规范的 Microsoft CLI 实现将所有写入视为已经具有释放栅栏语义。不过,CLI 的另一个实现可能仍然可以执行此优化。