Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lazy loading and the use of Thread.MemoryBarrier

When designing a class that has a reference to another object it might be beneficial to only create the referenced object the first time it is used, e.g. use lazy loading.

I often use this pattern to create a lazy loaded property:

Encoding utf8NoBomEncoding;

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

Then I came across this code when browsing the source code for the BCL:

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

As far as I can tell none of these are thread safe. E.g. multiple Encoding objects may be created. I completely get that and know that it isn't a problem if an extra Encoding object is created. It is immutable and will soon be garbage collection.

However, I'm really curious to understand why Thread.MemoryBarrier is necessary and how the second implementation is different from the first in multi-threading scenarios.

Obviously, if thread safety is a concern the best implementation is probably to use Lazy<T>:

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

Encoding Utf8NoBomEncoding {
  get {
    return this.lazyUtf8NoBomEncoding.Value;
  }
}
like image 779
Martin Liversage Avatar asked Feb 22 '23 11:02

Martin Liversage


1 Answers

This code would be a disaster without the memory barrier. Look closely at these lines of code.

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

Now, imagine some other thread sees the effects of the last line but doesn't see the effects of the first line. That would be a complete disaster.

The memory barrier ensures that any thread that sees encoding at all also sees all the effects of its constructor.

For example, without the memory barrier, the first and last lines could be internally optimized (roughly) as follows:
1) Allocate some memory, store a pointer to it in this.utf8NoBomEncoding
2) Call the UTF8Encoding constructor to fill in that memory with valid values.

Imagine if between steps 1 and 2 another thread runs and passes through this code. It will use an object that has not been constructed yet.

like image 175
David Schwartz Avatar answered Feb 25 '23 02:02

David Schwartz