延迟加载和使用Thread.MemoryBarrier

时间:2011-11-09 15:23:01

标签: c# .net multithreading lazy-loading memory-barriers

在设计具有对另一个对象的引用的类时,仅在第一次使用时创建引用的对象可能是有益的,例如,使用延迟加载。

我经常使用这种模式来创建一个延迟加载的属性:

Encoding utf8NoBomEncoding;

Encoding Utf8NoBomEncoding {
  get {
    return this.utf8NoBomEncoding ?? 
      (this.utf8NoBomEncoding = new UTF8Encoding(false));
  }
}

然后我在浏览BCL的源代码时遇到了这段代码:

Encoding Utf8NoBomEncoding {
  get {
    if (this.utf8NoBomEncoding == null) {
      var encoding = new UTF8Encoding(false);
      Thread.MemoryBarrier();
      this.utf8NoBomEncoding = encoding;
    }
    return this.utf8NoBomEncoding;
  }
}

据我所知,这些都不是线程安全的。例如。可以创建多个Encoding个对象。我完全明白了,如果创建了一个额外的Encoding对象,那就不是问题了。它是不可变的,很快就会被垃圾收集。

但是,我真的很想知道为什么Thread.MemoryBarrier是必要的,以及第二种实现与多线程场景中的第一种实现有何不同。

显然,如果线程安全是一个问题,最好的实现可能是使用Lazy<T>

Lazy<Encoding> lazyUtf8NoBomEncoding = 
  new Lazy<Encoding>(() => new UTF8Encoding(false));

Encoding Utf8NoBomEncoding {
  get {
    return this.lazyUtf8NoBomEncoding.Value;
  }
}

2 个答案:

答案 0 :(得分:6)

这段代码将是一场没有内存障碍的灾难。仔细看看这些代码行。

  var encoding = new UTF8Encoding(false);
  Thread.MemoryBarrier();
  this.utf8NoBomEncoding = encoding;

现在,想象一下其他一些线程看到最后一行的效果但看不到第一行的效果。那将是一场彻底的灾难。

内存屏障确保任何看到encoding的线程都能看到其构造函数的所有效果。

例如,在没有内存屏障的情况下,第一行和最后一行可以在内部优化(粗略地)如下:
1)分配一些内存,在this.utf8NoBomEncoding中存储指向它的指针 2)调用UTF8Encoding构造函数,用有效值填充该内存。

想象一下,如果在步骤1和步骤2之间运行另一个线程并通过此代码。它将使用尚未构建的对象。

答案 1 :(得分:2)

这种模式在.NET中相当常见。这是可能的,因为UTF8Encoding是一个不可变类。是的,可以创建多个类的实例,但这并不重要,因为所有实例都是相同的。使用Equals()覆盖强制执行。额外的副本将很快被垃圾收集。内存屏障只是确保对象状态完全可见。