SoFunction
Updated on 2025-03-06

A brief discussion on the implementation and performance comparison of C# singleton mode

Introduction

Singleton refers to a class that can only exist one instance (in C#, more precisely, there can only exist one instance in each AppDomain. It is one of the most used patterns in software engineering. After the first user creates an instance of this class, those who need to use this class can only use the previously created instances and cannot create a new instance. Usually, singletons will be created when they are used for the first time. This article introduces the implementation of several singletons in C# and analyzes the thread safety and performance differences between them.

There are many ways to implement singletons, but from the simplest implementations (non-delayed loading, non-thread-safe, inefficient), to delayed loading, thread-safe, and efficient implementations, they all have some basic commonalities:

  • Singleton classes have only one private parameterless constructor
  • Class declared as sealed (not required)
  • There is a static variable in the class that holds references to the created instance
  • The singleton class provides a static method or property to return a reference to the created instance ()

Several implementations

A 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();
      }
      return instance;
    }
  }
}

This method is not thread-safe. There will be two threads that execute if (instance == null) at the same time and create two different instances. The created ones will replace the newly created ones, resulting in the reference you received before being empty.

Two simple thread-safe implementation

public sealed class Singleton
{
  private static Singleton instance = null;
  private static readonly object padlock = new object();

  Singleton()
  {
  }

  public static Singleton Instance
  {
    get
    {
      lock (padlock)
      {
        if (instance == null)
        {
          instance = new Singleton();
        }
        return instance;
      }
    }
  }
}

Compared with implementation one, this version adds a lock to instance. Before calling instance, the padlock must be locked first, which avoids thread conflicts in implementation one. This implementation will only create an instance from beginning to end. However, since the lock is used every time Instance is called, and the overhead of calling the lock is relatively large, this implementation will have certain performance losses.

Note that we are using a new private object instance padlock to implement lock operation instead of directly locking Singleton. There is a potential risk when locking a type directly, because this type is public, so in theory it will be called in any code. Locking it directly will cause performance problems and may even cause deadlocks.

Note: In C#, the same thread can lock an object multiple times, but if different threads lock at the same time, thread waiting may occur, or in severe cases, deadlock may occur. Therefore, when using lock, we try to select private variables in the class to lock, so as to avoid the above situation.

Thread-safe implementation of triple-dual verification

public sealed calss Singleton
{
  private static Singleton instance = null;
  private static readonly object padlock = new object();

  Singleton()
  {
  }

  public static Singleton Instance
  {
    get
    {
      if (instance == null)
      {
        lock (padlock)
        {
          if (instance == null)
          {
            instance = new Singleton();
          }
        }
      }
      return instance;
    }
  } 
}

While ensuring thread safety, this implementation also avoids locking operations every time Instance is called, which will save a certain amount of time.

However, this implementation also has its disadvantages:

1 Cannot work in Java. (The specific reason can be found in the original text, I don’t understand it here)

2 Programmers are prone to errors when implementing it themselves. If you modify the code of this pattern yourself, be careful, because the logic of double check is relatively complicated and it is easy to make mistakes if you think poorly.

Four thread-safe implementations without locks

public sealed class Singleton
{
  //Initialization of instance will be performed when Singleton is called for the first time  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
    {
      return instance;
    }
  }
}

This implementation is simple and does not use locks, but it is still thread-safe. A static, readonly Singleton instance is used here. It will create a new instance when Singleton is called for the first time. The thread safety guarantee during the new creation is directly controlled by .NET. We can think that it is an atomic operation, and it will only be created once in an AppDomaing.

This implementation also has some disadvantages:

1Instance is created at unknown time, any call to Singleton will create instance in advance
2 static constructor loop call. If there are two classes A and B, B is called B in the static constructor of A, and A is called again in the static constructor of B, these two will form a loop call, which will cause the program to crash in serious cases.
3 We need to manually add Singleton's static constructor to ensure that the Singleton type will not be automatically added to the beforefieldinit Attribute, so as to ensure that the instance will be created only when Singleton is called the first time.
4readonly's attribute cannot be changed at runtime. If we need to dispose the instance and recreate a new instance when the program is running, this implementation method cannot be satisfied.

Five fully lazy instantiation

public sealed class Singleton
{
  private Singleton()
  {
  }

  public static Singleton Instance 
  {
    get
    {
      return ;
    }
  }

  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();
  }
}

Realize the five is to realize the four packaging. It ensures that instance will only be called in the Instance get method and will only be initialized before the first call. It is the version that ensures delayed loading that implements four.

6. Lazy<T> type using .NET4

public sealed class Singleton
{
  private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton());

  public static Singleton Instance 
  {
    get 
    {
      return ;
    }
  }

  private Singleton()
  {
  }
}

.NET4 or above supports Lazy<T> to implement lazy loading, which ensures the thread safety and latency loading characteristics of singletons with the cleanest code.

Performance differences

In previous implementations, we have all emphasized the thread safety and delayed loading of the code. However, in actual use, if the initialization of your singleton class is not a time-consuming operation or the initialization sequence will not cause bugs, delaying initialization is an optional feature because the time taken for initialization is negligible.

In actual usage scenarios, if your singleton instance is called frequently (such as in a loop), the performance consumption brought about in order to ensure thread safety is more worthy of attention.

In order to compare the performance of these implementations, I did a small test, looping to take 900 million single cases in these implementations, each time I call the instance method to perform a count++ operation, and output every one million times. The running environment is Visual Studio for Mac on MBP. The results are as follows:

Thread safety Delay loading Test run time (ms)
Realize one no yes 15532
Implementing two yes yes 45803
Realize three yes yes 15953
Realize four yes Not exactly 14572
Realize five yes yes 14295
Realize six yes yes 22875

The test method is not rigorous, but it can still be seen that since the lock is called every time, it is the most time-consuming, almost three times that of the other few. The second place is to use the .NET Lazy type implementation, which is about half more than the others. There is no obvious difference between the remaining four.

Summarize

Generally speaking, the various singleton implementation methods mentioned above are not much different from the performance of today's computers. Unless you need to call instances with a particularly large amount of concurrency, you need to consider the performance of locks.

For ordinary developers, it is good enough to use method two or six to implement singletons, while methods four and five require a good understanding of the C# operation process, and certain skills are required to be mastered during implementation, and the time they save is still limited.

Quote

Most of this article is translated fromImplementing the Singleton Pattern in C#,Added some of my own understanding. This is my searchstatic readonly field initializer vs static constructor initializationI would like to express my gratitude to the two authors here.

The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.