Three ages of Singleton pattern
The Singleton pattern appeared, perhaps, as soon as static objects appeared. In Smalltalk-80, ChangeSet was made this way, and sessions, statuses, and similar objects began to appear in a variety of libraries, which were united by one thing - they had to be the only ones in the entire program.
In 1994, the famous book Design Patterns was published, introducing the public, among 22 others, and our hero, who is now called Singleton. His C ++ implementation was there, like this:
As for flows, the authors do not even write about them, considering this problem of little relevance. But much attention was paid to all the subtleties of the inheritance of such classes from each other.
No wonder - it was 1995, and multitasking operating systems were too slow to embarrass anyone.
In any case, this code does not age. Use a similar implementation always if the class you want to declare will obviously not be called from multiple threads.
In 1995, Scott Myers released his second book on the tricks of C ++. Among other things, he encourages her to use Singleton instead of static classes - to save memory and know exactly when its constructor will execute.
It was in this book that the canonical singleton Myers appeared and I see no reason not to bring it here:
Neatly, concisely and skillfully beaten the standard of the 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 a little more operations:
According to the new C ++ 11 standard, nothing more is needed to support threads. But all compilers still need to live up to its full support.
In the meantime, for at least fifteen years, the best minds have tried to catch multi-threaded singleton in the cell of linguistic syntax. C ++ did not support streams without third-party libraries - so very soon almost every library with streams came up with its own Singleton, which was “better than all the others”. Alexandrescu devotes a whole chapter to them, domestic developers are struggling with him not for life, but for death, and someone Andrei Nasonov also experimented for a long time and eventually ... offers a completely different solution.
In 2004, Meyers and Alexandrescu joined forces to describe Singleton with Double-check locking. The idea is simple - if the singleton is not found in the first if, we make a lock and check it again inside.
In the meantime, the trial and the case, the problem of thread-safe Singleton crawled into other C-like languages. First, in Java, and then in C #. And now John Skeet offers a whole range of solutions, each of which has its pros and cons. And they are also offered by Microsoft.
For starters - the same option with double-check locking. Microsoft advises writing it like this:
Skeet, however, believes this code is bad. Why?
- This does not work in Java. The Java memory model prior to version 1.5 did not check if the constructor completed before assigning a value. Fortunately, this is no longer relevant - Java 1.7 was released long ago, and Microsoft recommends this code and guarantees that it will work.
“It's easy to break it.” You will get confused in brackets - and that’s it.
- Because of the lock, it is rather slow.
- There are better.
There were also options without using streaming interfaces.
In particular, the well-known implementation through the readonly field. According to Skeet (and Microsoft), this is the first noteworthy option: Here's what it looks like:
This option is also thread-safe and is based on the curious property of readonly fields - they are not initialized immediately, but on the first call. A wonderful idea, and the author himself recommends using it.
Does this implementation have any disadvantages? Of course, yes:
- If the class has static methods, then when they are called readonly, the field is initialized automatically.
- The constructor can only be static. This is a compiler feature - if the constructor is not static, then the type will be marked as beforefieldinit and readonly will be created simultaneously with the static ones.
- The static constructors of several related Singletons can inadvertently loop each other, and then nothing will help and no one will save.
Finally, the famous lazy implementation with a nested class.
Its weaknesses are the same as any other code that uses nested classes.
In recent versions of C #, the System.Lazy class has appeared, which all of these are encapsulated. So, the implementation has become even shorter:
It is easy to see that implementations with readonly, and the variant with the nested class, and its simplification in the form of a lazy object do not work with streams. 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 streams.
Why is it not good to “deceive” language? Because each such "hack" is very easy to accidentally break. And because there is no benefit to him from 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 built-in classes and whole keywords for working 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 deploys such a lock (obj):
something like this code:
Jeffrey Richter considers this code very unsuccessful. 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, then the error will most likely happen again. Therefore, he calls for using Monitor.Enter / Monitor.Exit for regular threads, and rewriting Singleton on atomic operations. Like this:
A temporary variable is needed because the C # standard requires the compiler to create the variable first and then assign it. As a result, it may turn out that instance is no longer null, but the initialization of singleton has not yet been completed. See case history in the 29th chapter of CLR via C # by Jeffrey Richter, section The Famous Double-Check Locking Technique .
Thus, there was a place and double-lock.
Use this option for multi-threaded cases . It is simple, does not do anything poorly documented, it is difficult to break, and it can easily be transferred to any language where there are atomic operations.
In 1994, the famous book Design Patterns was published, introducing the public, among 22 others, and our hero, who is now called Singleton. His C ++ implementation was there, 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 flows, the authors do not even write about them, considering this problem of little relevance. But much attention was paid to all the subtleties of the inheritance of such classes from each other.
No wonder - it was 1995, and multitasking operating systems were too slow to embarrass anyone.
In any case, this code does not age. Use a similar implementation always if the class you want to declare will obviously not be called from multiple threads.
In 1995, Scott Myers released his second book on the tricks of C ++. Among other things, he encourages her to use Singleton instead of static classes - to save memory and know exactly when its constructor will execute.
It was in this book that the canonical singleton Myers 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, concisely and skillfully beaten the standard of the 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 a little 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 still need to live up to its full support.
In the meantime, for at least fifteen years, the best minds have tried to catch multi-threaded singleton in the cell of linguistic syntax. C ++ did not support streams without third-party libraries - so very soon almost every library with streams came up with its own Singleton, which was “better than all the others”. Alexandrescu devotes a whole chapter to them, domestic developers are struggling with him not for life, but for death, and someone Andrei Nasonov also experimented for a long time and eventually ... offers a completely different solution.
In 2004, Meyers and Alexandrescu joined forces to describe Singleton with Double-check locking. The idea is simple - if the singleton is not found in the first if, we make a lock and check it again inside.
In the meantime, the trial and the case, the problem of thread-safe Singleton crawled into other C-like languages. First, in Java, and then in C #. And now John Skeet offers a whole range of solutions, each of which has its pros and cons. And they are also offered by Microsoft.
For starters - the same option with double-check locking. Microsoft advises writing 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;
}
}
}
Skeet, however, believes this code is bad. Why?
- This does not work in Java. The Java memory model prior to version 1.5 did not check if the constructor completed before assigning a value. Fortunately, this is no longer relevant - Java 1.7 was released long ago, and Microsoft recommends this code and guarantees that it will work.
“It's easy to break it.” You will get confused in brackets - and that’s it.
- Because of the lock, it is rather slow.
- There are better.
There were also options without using streaming interfaces.
In particular, the well-known implementation through the readonly field. According to Skeet (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 is based on the curious property of readonly fields - they are not initialized immediately, but on the first call. A wonderful idea, and the author himself recommends using it.
Does this implementation have any disadvantages? Of course, yes:
- If the class has static methods, then when they are called readonly, the field is initialized automatically.
- The constructor can only be static. This is a compiler feature - if the constructor is not static, then the type will be marked as beforefieldinit and readonly will be created simultaneously with the static ones.
- The static constructors of several related Singletons can inadvertently loop 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();
}
}
Its weaknesses are the same as any other code that uses nested classes.
In recent versions of C #, the System.Lazy class has appeared, which all of these are encapsulated. So, the implementation has become even shorter:
public sealed class Singleton
{
private static readonly Lazy lazy =
new Lazy(() => new Singleton());
public static Singleton Instance { get { return lazy.Value; } }
private Singleton()
{
}
}
It is easy to see that implementations with readonly, and the variant with the nested class, and its simplification in the form of a lazy object do not work with streams. 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 streams.
Why is it not good to “deceive” language? Because each such "hack" is very easy to accidentally break. And because there is no benefit to him from 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 built-in classes and whole keywords for working 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 deploys such a lock (obj):
lock(this) {
// other code
}
something like 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 unsuccessful. 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, then the error will most likely happen again. Therefore, he calls for using Monitor.Enter / Monitor.Exit for regular threads, and rewriting Singleton on 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 create the variable first and then assign it. As a result, it may turn out that instance is no longer null, but the initialization of singleton has not yet been completed. See case history in the 29th chapter of CLR via C # by Jeffrey Richter, section The Famous Double-Check Locking Technique .
Thus, there was a place and double-lock.
Use this option for multi-threaded cases . It is simple, does not do anything poorly documented, it is difficult to break, and it can easily be transferred to any language where there are atomic operations.