cho*_*ppy 3 c# concurrency singleton volatile double-checked-locking
fallowing子句来自jetbrains.net在阅读了这篇以及网上的其他文章后,我仍然不明白在第一个线程进入锁之后如何返回null.有人确实理解它可以帮助我并以更人性化的方式解释它吗?
"考虑以下代码:
public class Foo
{
private static Foo instance;
private static readonly object padlock = new object();
public static Foo Get()
{
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
instance = new Foo();
}
}
}
return instance;
}
};
Run Code Online (Sandbox Code Playgroud)
给定上面的代码,初始化Foo实例的写入可以被延迟,直到写入实例值,从而产生实例返回处于单元化状态的对象的可能性.
为了避免这种情况,必须使实例值易变."
Bri*_*eon 12
回归null不是问题.问题是新实例可能处于另一个线程所感知的部分构造状态.考虑一下这个宣言Foo.
class Foo
{
public int variable1;
public int variable2;
public Foo()
{
variable1 = 1;
variable2 = 2;
}
}
Run Code Online (Sandbox Code Playgroud)
以下是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;
Run Code Online (Sandbox Code Playgroud)
首先,请注意构造函数是内联的(因为它很简单).现在,希望很容易看到instance在构造函数内部初始化其组成字段之前获得引用.这是一个有效的策略,因为只要读取和写入不通过lock或改变逻辑流程,读取和写入就可以自由地上下浮动; 他们没有.因此,另一个线程可以instance != null在完全初始化之前查看并尝试使用它.
volatile解决了这个问题,因为它将读取视为获取栅栏并写入释放栅栏.
因此,如果我们标记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;
Run Code Online (Sandbox Code Playgroud)
对组成变量的写入Foo仍然可以重新排序,但请注意,内存屏障现在可以防止它们在分配后发生instance.使用箭头作为指导,想象允许和禁止的各种不同的优化策略.请记住,不允许任何读取或写入通过↑箭头向上浮动或向上移动超过↓箭头.
Thread.VolatileWrite也可以解决这个问题,并且可以在没有volatile像VB.NET这样的关键字的语言中使用.如果你看看如何VolatileWrite实现你会看到这个.
public static void VolatileWrite(ref object address, object value)
{
Thread.MemoryBarrier();
address = value;
}
Run Code Online (Sandbox Code Playgroud)
现在,这可能看起来反直觉.毕竟,记忆障碍是在作业之前放置的.如何将作业提交给你要求的主内存?在作业完成后放置障碍物会不正确?如果这就是你的直觉告诉你的话那就错了.你看到记忆障碍并非严格意义上的"新鲜阅读"或"承诺写作".这完全是关于指令的排序.这是迄今为止我看到的最大混乱的根源.
提到Thread.MemoryBarrier实际产生全栅栏障碍也许是很重要的.所以,如果我用箭头上面的符号,那么它看起来就像这样.
public static void VolatileWrite(ref object address, object value)
{
? // full barrier
? // full barrier
address = value;
}
Run Code Online (Sandbox Code Playgroud)
因此,技术上的召唤VolatileWrite不仅仅是对volatile字段的写入所做的事情.请记住,volatile例如,VB.NET中不允许这样做,但它VolatileWrite是BCL的一部分,因此可以在其他语言中使用.
1 这种优化主要是理论上的.ECMA规范在技术上允许它,但ECMA规范的Microsoft CLI实现将所有写入视为已经具有发布范围语义.CLI的另一个实现可能仍然可以执行此优化.