辛格尔顿仔细检查并发问题 [英] Singleton double-check concurrency issue

查看:94
本文介绍了辛格尔顿仔细检查并发问题的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

休耕子句从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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆