Initial comments
I donโt necessarily think that the author of this article actually suggested using this variation of the double-checked lock pattern. I think he simply pointed out that this is one option that can be considered by a naive developer to solve the problem in the context of value types.
Value types, obviously, cannot store null
values, so you need to use another variable to confirm the completion of initialization. The author mentions all this, and then vaguely talks about reading instance
as null
. Presumably, the author was thinking of a really naive developer who used this change incorrectly by value type at a time, and then continued to apply it, and also incorrectly, for reference types. In the case of a value type, the stream can read and use struct
with initialization of the default field, if it is not intended. In the case of reference types, a stream can read and use an instance of null
.
The use of Thread.VolatileRead
was proposed by the author to correct this option. Without a volatile read, reading instance
in the return statement can be removed before reading initialized
as follows.
class Singleton { private static object slock = new object(); private static Singleton instance; private static int initialized; private Singleton() {} public Instance { get { var local = instance; if (initialized == 0) { lock (slock) { if (initialized == 0) { instance = new Singleton(); initialized = 1; } } } return local; } } }
We hope that the aforementioned reordering of the code clearly demonstrates the problem. And it is obvious that an intermittent reading of initialized
prevents instance
reading.
And again, I think that the author simply showed one of the possible ways to correct this particular option, and not that the author advocated this approach as a whole.
Answering your questions
My question is, is there still a danger that recordings will be ordered?
YES (qualified): since you correctly pointed out that the records in instance
and initialized
can be replaced inside lock
. Worse, entries that can occur inside Singleton.ctor
can also fail so that instance
assigned before the instance is fully initialized. Another thread might see the instance
set, but this instance may be in a partially constructed state.
However, Microsoft entries for CLI implementations have release fence semantics. The meaning of everything I just said does not apply when using the .NET Framework runtime on any hardware platform. But an obscure environment such as Mono running on ARM may exhibit problematic behavior.
An author using Thread.VolatileRead
to โfixโ this option will not work at all because he does nothing to solve the reordered write problem. The code is not 100% portable. This is one of the reasons why I doubt that the author suggested this variation.
The canonical variation of using a single instance
variable in combination with volatile
is obviously the right solution. The volatile
keyword has the semantics of capture semantics when reading and the release semantics for writing, so it solves both problems; the one you identified and the one that is listed in the article.