4

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

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

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

2 回答 2

6

如果没有内存屏障,这段代码将是一场灾难。仔细查看这些代码行。

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

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

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

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

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

于 2011-11-09T15:35:05.267 回答
2

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

于 2011-11-09T15:54:46.560 回答