Introduction to the
Singletons are classes that can only have one instance (in C#, more accurately, classes that can only have one instance per AppDomain) and are one of the most commonly used patterns in software engineering. After the first user creates an instance of the class, subsequent users can only use the previously created instance and cannot create a new one. Typically, a singleton is created the first time it is used. This article introduces several implementations of singletons in C# and analyzes the thread-safety and performance differences between them.
There are many ways to implement singletons, but from the simplest implementation (non-lazy loading, non-thread-safe, inefficient) to the lazy loading, thread-safe, and efficient implementation, they all have some basic things in common:
The singleton class has only one private, parameterless constructor. The class declaration is sealed (not required). A class has a static variable that holds a reference to the instance created. A singleton class provides a static method or property that returns a reference to the instance created (eg.getInstance).Copy the code
Several implementations
Non-thread-safe
//Bad code! Do not use!
public sealed class Singleton
{
private static Singleton instance = null;
private Singleton()
{}public static Singleton instance
{
get
{
if (instance == null)
{
instance = new Singleton();
}
returninstance; }}}Copy the code
This method is not thread-safe. Two threads will execute if (instance == null) and create two different instances, which will replace the newly created instance and result in null reference.
Simple thread-safe implementation
public sealed class Singleton
{
private static Singleton instance = null;
private static readonly object padlock = new object(a); Singleton() { }public static Singleton Instance
{
get
{
lock (padlock)
{
if (instance == null)
{
instance = new Singleton();
}
returninstance; }}}}Copy the code
Compared to implementation 1, this version adds a lock on instance, which locks padlock before calling instance. This avoids thread collisions in implementation 1, which creates only one instance from start to finish. However, because the lock is used every time Instance is called, and the overhead of calling the lock is high, this implementation incurs a performance penalty.
Note that instead of locking the Singleton directly, we use a new private object instance padlock. Locking a type directly is potentially risky. Because the type is public, it can theoretically be called in any code, and locking it directly can cause performance problems and even deadlocks.
Note: in C#, it is possible for the same thread to lock an object more than once, but if different threads are locked at the same time, the thread may wait, or even deadlock. Therefore, when using lock, we try to choose the private variables in the class to lock, so as to avoid this situation.
Thread-safe implementation of dual authentication
public sealed calss Singleton
{
private static Singleton instance = null;
private static readonly object padlock = new object(a); Singleton() { }public static Singleton Instance
{
get
{
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
instance = newSingleton(); }}}returninstance; }}}Copy the code
While keeping the thread safe, this implementation also saves time by avoiding the lock operation every time Instance is called.
However, this implementation has its drawbacks:
1. Does not work in Java. 2. Programmers are prone to make mistakes when implementing their own programs. Be careful if you make your own changes to the code for this pattern, because double Check's logic is complex and can easily go wrong.Copy the code
Thread-safe implementation without locking
public sealed class Singleton
{
Instance initialization is performed the first time a Singleton is called
private static readonly Singleton instance = new Singleton();
//Explicit static consturctor to tell C# compiler
//not to mark type as beforefieldinit
static Singleton()
{}private Singleton()
{}public static Singleton Instance
{
get
{
returninstance; }}}Copy the code
This implementation is simple and does not use locks, but it is still thread-safe. A static, readonly Singleton instance is used to create a new instance when the Singleton is called for the first time. NET directly controls, we can think of it as an atomic operation, and it will only be created once in an AppDomaing.
There are some downsides to this implementation:
Instance is created at an unknown time, and any call to the Singleton will create instance ahead of time. If there are two classes A and B, the static constructor of A calls B, and the static constructor of B calls A, the two will form A circular call, which will cause the program to crash. 3. We need to manually add the static constructor of the Singleton to ensure that the Singleton type is not automatically assigned the beforefieldinit Attribute to ensure that instance is created on the first call to the Singleton. 4. The property of readOnly cannot be changed at run time. If dispose instance and create a new instance when the program is running, this implementation method cannot be satisfied.Copy the code
5. Fully lazy Instantiation
public sealed class Singleton
{
private Singleton()
{}public static Singleton Instance
{
get
{
returnNested.instance; }}private class Nested
{
// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Nested()
{}internal static readonly Singleton instance = newSingleton(); }}Copy the code
Realizing five is realizing the packaging of four. It ensures that instance is called only in the instance get method and initialized only before the first call. It is the lazy-loaded version of implementation four.
Use.net4 Lazy
public sealed class Singleton
{
private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton());
public static Singleton Instance
{
get
{
returnlazy.Value; }}private Singleton()
{}}Copy the code
Versions of.net 4 and above support Lazy for Lazy loading, which guarantees singleton thread safety and Lazy loading features with minimal code.
Performance difference
In previous implementations, we emphasized thread-safety and lazy loading of code. In practice, however, if the initialization of your singleton class is not a time-consuming operation or the initialization sequence does not cause bugs, delayed initialization is an optional feature because the initialization time is negligible.
In a real-world scenario, if your singleton instance will be called frequently (such as in a loop), the performance cost of keeping it thread-safe is of greater concern.
To compare the performance of these implementations, I ran a small test that looped through the singletons of these implementations 900 million times, calling the instance method to perform count++ every one million times, running in Visual Studio for Mac on MBP. The results are as follows:
Thread safety | Lazy loading | Test Run time (ms) | |
---|---|---|---|
Implement a | no | is | 15532 |
Realize the | is | is | 45803 |
Implement three | is | is | 15953 |
Realization of the four | is | Don’t fully | 14572 |
Implement five | is | is | 14295 |
To implement six | is | is | 22875 |
The test method is not rigorous, but it can still be seen that method 2 is the most time-consuming, almost three times as time-consuming as the others, due to the need to call lock every time. Second on the list is use. NET Lazy is implemented about half as much as the others. For the other four, there was no significant difference.
conclusion
In general, all of the above singleton implementations don’t make much of a difference in today’s computer performance, unless you need to call instance with a particularly large number of concurrent calls to consider locking performance.
Methods two or six are good enough for the average developer to implement singletons, methods four and five require a good understanding of C# flow and some skill in implementation, and their time savings are still limited.
reference
Most of this paper is a translation of the self-implementing Singleton Pattern in C#, with some of my own understanding. This is what I saw when I searched static Readonly Field Initializer vs Static Constructor Initialization, and I want to thank both authors.