单例双重检查并发问题 [英] Singleton double-check concurrency issue

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

问题描述

休耕条款取自 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?

"考虑以下代码:

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;
  }
};

给定上面的代码,初始化 Foo 实例的写入可能会延迟到写入实例值,从而创建实例返回处于未初始化状态的对象的可能性.

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. "

推荐答案

返回 null 不是问题.问题是新实例可能处于另一个线程感知的部分构造状态.考虑一下 Foo 的这个声明.

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;
  }
}

以下是如何通过 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;

首先,注意构造函数是内联的(因为它很简单).现在,希望很容易看到 instance 在其组成字段在构造函数中初始化之前被分配了引用.这是一个有效的策略,因为只要读和写不越过lock 的边界或改变逻辑 流程,它们就可以自由地上下浮动;他们没有.所以另一个线程可以看到 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 修复了这个问题,因为它将读取视为获取栅栏,而将写入视为释放栅栏.

volatile fixes this issue because it treats reads as an acquire fence and writes as a release fence.

  • acquire-fence:其他读取和操作的内存屏障不允许写在围栏前移动.
  • release-fence:一个内存屏障,其他读取和不允许写在围栏之后移动.

因此,如果我们将 instance 标记为 volatile,那么释放栅栏将阻止上述优化.以下是带有屏障注释的代码的外观.我使用 ↑ 箭头表示释放栅栏,使用 ↓ 箭头表示获取栅栏.请注意,任何东西都不允许向下浮动超过 ↑ 箭头或向上浮动超过 ↓ 箭头.把箭头想象成把一切都推开.

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;

Foo 的组成变量的写入仍然可以重新排序,但请注意,内存屏障现在阻止它们在分配给 instance 后发生.使用箭头作为指导,想象允许和不允许的各种不同的优化策略.请记住,不允许读取写入向下浮动超过 ↑ 箭头或向上浮动超过 ↓ 箭头.

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 也可以解决这个问题,并且可以在没有 volatile 关键字的语言中使用,如 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 比写入 volatile 字段所做的要多.请记住,例如 volatile 在 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 规范在技术上确实允许这样做,但 ECMA 规范的 Microsoft CLI 实现将所有写入视为已经具有发布栅栏语义.不过,CLI 的另一个实现可能仍然可以执行此优化.

这篇关于单例双重检查并发问题的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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