📜 ⬆️ ⬇️

Three Ages of the Singleton Pattern

The Singleton pattern appeared, perhaps, as soon as static objects appeared. In Smalltalk-80, ChangeSet was made this way, and slightly in various libraries, sessions, statuses and similar objects began to appear, which were united by one thing - they were supposed to be the only ones for the entire program.

In 1994, the well-known book “Design Patterns” was published, introducing to the public, among 22 others, and our hero, who is now called Singleton. There was also its implementation in C ++, like this:


//.h class Singleton { public: static Singleton* Instance(); protected: Singleton(); private: static Singleton* _instance; } //.cpp Singleton* Singleton::_instance = 0; Singleton* Singleton::Instance() { if(_instance == 0){ _instance = new Singleton; } return _instance; } 

As for streams, the authors do not even write about them, considering this problem to be of minor relevance. But a lot of attention was paid to all the subtleties of inheriting such classes from each other.
')
No wonder - the year was 1995 and multitasking operating systems were too slow to embarrass anyone.

In any case, this code does not age. Use such an implementation always if the class you want to declare will not be called from several threads.

In 1995, Scott Myers released his second book on C ++ Tricks. Among other things, he urges it to use Singleton instead of static classes - to save memory and know exactly when its constructor will be executed.

It was in this book that the canonical Myers singleton appeared and I see no reason not to bring it here:
 class singleton { public: static singleton* instance() { static singleton inst; return &inst; } private: singleton() {} }; 


Neatly, succinctly and skillfully beat the standard language. A local static variable in a function will be called if and only if the function itself is called.

Then it was expanded, banning slightly more operations:

 class CMySingleton { public: static CMySingleton& Instance() { static CMySingleton singleton; return singleton; } // Other non-static member functions private: CMySingleton() {} // Private constructor ~CMySingleton() {} CMySingleton(const CMySingleton&); // Prevent copy-construction CMySingleton& operator=(const CMySingleton&); // Prevent assignment }; 

According to the new C ++ 11 standard, nothing more is needed to support threads. But all compilers need to live to its full support.

In the meantime, for at least a decade and a half, the best minds have tried to catch a multi-threaded singleton into a cell of language syntax. C ++ did not support threads without third-party libraries - so very soon almost every single library with threads had its own Singleton, which was “better than everyone else.” Alexandrescu devotes a whole chapter to them, domestic developers struggle with him not for life, but for death, and someone Andrei Nasonov has been experimenting for a long time and as a result offers ... a completely different solution.

In 2004, Meyers and Alexandrescu joined forces and described Singleton with Double-check locking. The idea is simple - if the singleton is not found in the first if-e, we do the lock, and already inside we check it again.

In the meantime, the court case, the problem of stream-safe Singleton has crept into other C-like languages. First, in Java, and then in C #. And now John Skeat offers a whole range of solutions, each of which has its advantages and disadvantages. And they are offered by Microsoft.

To begin with - the same option with double-check locking. Microsoft advises to write it like this:
 using System; public sealed class Singleton { private static volatile Singleton instance; private static object syncRoot = new Object(); private Singleton() {} public static Singleton Instance { get { if (instance == null) { lock (syncRoot) { if (instance == null) instance = new Singleton(); } } return instance; } } } 


Skit, however, believes that this code is bad. Why?

- It does not work in Java. The Java memory model prior to version 1.5 did not check whether the execution of the constructor completed before assigning a value. Fortunately, this is no longer relevant - Java 1.7 has long been released, and Microsoft recommends this code and guarantees that it will work.
- It is easy to break. You get confused in brackets - that's all.
- Because of the lock, it is slow enough
- there are better

There were options without the use of streaming interfaces.

In particular, the well-known implementation through the readonly field. According to Skit (and Microsoft), this is the first noteworthy option: Here’s what it looks like:

 public sealed class Singleton { private static readonly Singleton instance = new Singleton(); // Explicit static constructor to tell C# compiler // not to mark type as beforefieldinit static Singleton() { } private Singleton() { } public static Singleton Instance { get { return instance; } } } 



This option is also thread-safe and based on the curious property of the readonly fields - they are initialized not immediately, but on the first call. Great idea, and the author himself recommends using it.

Does this implementation have flaws? Of course, yes:

- If the class has static methods, then when they are called readonly, the field is initialized automatically.
- A constructor can only be static. This is a feature of the compiler - if the constructor is not static, then the type will be marked as beforefieldinit and readonly created simultaneously with the static ones.
- Static constructors of several connected Singleton-s can randomly loop over each other, and then nothing will help and no one will save.

Finally, the famous lazy implementation with a nested class.
 public sealed class Singleton { private Singleton() { } public static Singleton Instance { get { return Nested.instance; } } private class Nested { // Explicit static constructor to tell C# compiler // not to mark type as beforefieldinit static Nested() { } internal static readonly Singleton instance = new Singleton(); } } 


It has the same drawbacks as any other code that uses nested classes.

In the latest versions of C #, the System.Lazy class has appeared, which all this has been encapsulated. So, the implementation has become even shorter:
 public sealed class Singleton { private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton()); public static Singleton Instance { get { return lazy.Value; } } private Singleton() { } } 


It is easy to see that both readonly implementations, the nested class variant, and its simplification in the form of a lazy object do not work with threads. Instead, they use the very structures of the language that “deceive” the interpreter. This is their most important difference from double-lock, which works with threads.

Why is it wrong to "deceive" the language? Because each such "hack" is very easy to accidentally break. And because there is no benefit from it to people who write in other languages ​​- and yet the pattern implies universality.

Personally, I think that the problem of flows should be solved by standard means. C # has many classes built in and whole keywords to work with multithreading. Why not use standard tools instead of trying to trick the compiler.

As I said, lock is not the best solution. The fact is that the compiler expands such a lock (obj):

 lock(this) { // other code } 


about this code:

 Boolean lockTaken = false; try { Monitor.Enter(this, ref lockTaken); // other code } finally { if(lockTaken) Monitor.Exit(this); } 


Jeffrey Richter considers this code very unfortunate. First, try is very slow. Secondly, if try crashed, then something is wrong in the code. And when the second thread starts to execute it, the error is likely to recur. Therefore, he calls for using Monitor.Enter / Monitor.Exit for regular streams, and Singleton to rewrite atomic operations. Like this:

 public sealed class Singleton { private static readonly Object s_lock = new Object(); private static Singleton instance = null; private Singleton() { } public static Singleton Instance { get { if(instance != null) return instance; Monitor.Enter(s_lock); Singleton temp = new Singleton(); Interlocked.Exchange(ref instance, temp); Monitor.Exit(s_lock); return instance; } } } 


A temporary variable is needed because the C # standard requires the compiler to first create a variable and then assign it. As a result, it may turn out that in the instance is no longer null, but the initialization of singleton has not yet been completed. See a description of similar cases in Chapter 29 of the CLR via C # Jeffrey Richter, section The Famous Double-Check Locking Technique .

Thus, there was a place and double-lock

Use this option for multithreaded cases . It is simple, does not do anything poorly documented, it is difficult to break it and it is easily transferred to any language where there are atomic operations.

Source: https://habr.com/ru/post/147373/


All Articles