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;
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With