辛格尔顿仔细检查并发问题 [英] Singleton double-check concurrency issue
问题描述
休耕子句从jetbrains.net取
读这篇文章,一些其他文章在网络上后,我还是不明白怎么可能返回null,则第一个线程进去了锁之后。
有一种人,不明白可以请帮我和更人性化的方式解释一下吗?
The fallowing clause is taken from jetbrains.net After reading this and some other articles on the web, I still don't understand how is it possible to return null, after the first thread go in to the lock. Some one that does understand it can please help me and explain it in more humanized way?
请考虑以下一段code的:
"Consider the following piece of code:
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;
}
};
鉴于上述code
写道,初始化美孚实例可以被推迟,直到实例值的写入,从而创建一个实例返回一个未初始化状态的对象的可能性。
Given the above code, writes that initialize the Foo instance could be delayed until the write of the instance value, thus creating the possibility that the instance returns an object in an unitialized state.
为了避免这种情况,该实例值必须进行挥发。
In order to avoid this, the instance value must be made volatile. "
推荐答案
返回空
不是问题。的问题是,新的实例可以是部分构造的状态由另一个线程所察觉。考虑本声明富
。
Returning null
is not the issue. The issue is that the new instance may be in a partially constructed state as perceived by another thread. Consider this declaration of Foo
.
class Foo
{
public int variable1;
public int variable2;
public Foo()
{
variable1 = 1;
variable2 = 2;
}
}
下面是怎么code可以由C#编译器,JIT编译器或硬件得到优化。 1
Here is how the code could get optimized by the C# compiler, JIT compiler, or hardware.1
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
instance = alloc Foo;
instance.variable1 = 1; // inlined ctor
instance.variable2 = 2; // inlined ctor
}
}
}
return instance;
首先,注意构造函数内联(因为它很简单)。现在,希望这是容易看到实例
被分配参考它的组成领域得到构造函数初始化之前。这是一个有效的策略,因为读取和写入可以自由上下浮动,只要他们没有通过锁的界限
或改变的逻辑流;而他们没有。因此,另一个线程可以看到实例!= NULL
,并尝试使用它,它完全初始化之前。
First, notice that the constructor is inlined (because it was simple). Now, hopefully it is easy to see that instance
gets assigned the reference before its constituent fields get initialized inside the constructor. This is a valid strategy because reads and writes are free to float up and down as long as they do not pass the boundaries of the lock
or alter the logical flow; which they do not. So another thread could see instance != null
and attempt to use it before it is fully initialized.
挥发性
修复这个问题,因为它把阅读作为的收购栅栏的和写为的发布栅栏的
volatile
fixes this issue because it treats reads as an acquire fence and writes as a release fence.
- 收购围栏:内存屏障中,其他读取和放大器;写操作是不允许的栅栏前搬迁。
- 发布围栏:内存屏障中,其他读取和放大器;写操作是不允许的围栏后移动。
所以,如果我们纪念实例
为挥发性
然后释放栅栏将prevent上述优化。这里是code会怎样看与障碍物的注解。我用了一个↑箭头指示释放围栏和↓箭头指示获取围栏。请注意,没有什么是允许下一个过去↑箭头或向上飘过去的一个↓箭头。想想箭头为推动所有一切的。
So if we mark instance
as volatile
then the release-fence will prevent the above optimization. Here is how the code would look with the barrier annotations. I used an ↑ arrow to indicate a release-fence and a ↓ arrow to indicate an acquire-fence. Notice that nothing is allowed to float down past an ↑ arrow or up past an ↓ arrow. Think of the arrow head as pushing everything away.
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;
要的组成变量写入富
仍然可以重新排序,但请注意,记忆障碍,现在$ P $从分配后发生到<$ C pvents他们$ C>实例。使用箭头作为指导想象被允许和不允许的各种不同的优化策略。请记住,没有读的或的写操作被允许上下浮动的过去↑箭头或向上的过去↓箭头。
The writes to the constituent variables of Foo
could still be reordered, but notice that the memory barrier now prevents them from occurring after the assignment to instance
. Using the arrows as a guide imagine various different optimization strategies that are allowed and disallowed. Remember that no reads or writes are allowed to float down past an ↑ arrow or up past an ↓ arrow.
Thread.VolatileWrite
就已经解决了这个问题,以及和能够在没有语言像挥发性
关键字可以使用VB.NET。如果你看一看如何 VolatileWrite
实现,你会看到这一点。
Thread.VolatileWrite
would have solved this problem as well and could be used in languages without a volatile
keyword like VB.NET. If you take a look at how VolatileWrite
is implemented you would see this.
public static void VolatileWrite(ref object address, object value)
{
Thread.MemoryBarrier();
address = value;
}
现在这似乎在第一直觉。毕竟,内存屏障的转让之前放置的。怎么样让致力于主内存你问的分配?岂不是更正确放置障碍物的 的分配后?如果这是它是什么你的直觉告诉你然后点击错误即可。你看内存壁垒不严格有关获取一个鲜读或写承诺。这是所有的指令排序。这是迄今为止混乱的最大来源我明白了。
Now this may seem counter intuitive at first. Afterall, the memory barrier is placed before the assignment. What about getting the assignment committed to main memory you ask? Would it not be more correct to place the barrier after the assignment? If that is what your intuition is telling you then it is wrong. You see memory barriers are not strictly about getting a "fresh read" or a "committed write". It is all about instruction ordering. This is by far the biggest source of confusion I see.
这也很重要一提的是 Thread.MemoryBarrier
实际生成一个完整的围栏屏障。所以,如果我使用上面的箭头符号我那么它会是这样的。
It might also be important to mention that Thread.MemoryBarrier
actually generates a full-fence barrier. So if I were to use my notation above with the arrows then it would look like this.
public static void VolatileWrite(ref object address, object value)
{
↑ // full barrier
↓ // full barrier
address = value;
}
所以,从技术上叫 VolatileWrite
确实比为挥发性
字段写入会做的更多。请记住,挥发性
在VB.NET中是不允许的例子,但 VolatileWrite
被拆开的BCL的所以可以在其他语言中使用。
So technically calling VolatileWrite
does more than what a write to a volatile
field would do. Remember that volatile
is not allowed in VB.NET for example, but VolatileWrite
is apart of the BCL so it can be used in other languages.
1 的这种优化主要是理论上的。在ECMA规范并在技术上允许,但微软CLI实现ECMA规范把所有写道,如果他们有释放栅栏语义了。这可能是在CLI的另一实现仍可虽然执行该优化的
这篇关于辛格尔顿仔细检查并发问题的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!