Design Pattern: Singleton - Class design vs Lazy<?T> vs Container

Design Pattern: Singleton - Class design vs Lazy<T> vs Container

What problems does singleton solve?

A singleton is a class that has been designed to only ever allow one instance of itself to be created. The class itself is responsible for enforcing this design requirement. Single instances of classes are often required to model some kind of shared resource, like the file system or a shared network resource, like a scanner or print spooler

ways to achieve Singleton

  1. Class design: to achieve the singleton behavior through the class design
  2. Lazy<T>: provide an elegant and easily understood approach
  3. DI containers: if you're working in a framework that supports dependency injection and IoC containers, they are usually the best place to manage the lifetime of the instances of your classes as opposed to putting that responsibility in the classes themselves

Singleton Structure

the singleton is pretty simple. It's just a single class with

  1. a private instance
  2. a public static method that provides the only way to reference that instance.
  3. ?It must have a private constructor

Singleton ?Features

  1. Singleton classes have at any time in the life of an application either 0 or 1 instance
  2. Singleton classes are created without parameters
  3. singleton instances are typically not created until something requests them. This is known as lazy instantiation
  4. a single, private, and?parameterless constructor
  5. a sealed class
  6. The only reference to the singleton should be in a private static field in the singleton class itself.

Singleton implementation - Class Design

the first version - not thread-safe

public sealed class Singleton
??? {
??????? private static Singleton? _instance;

??????? public static Singleton Instance
??????? {
??????????? get
??????????? {
??????????????? Logger.Log("Instance called.");
??????????????? return _instance ??= new Singleton();
??????????? }
??????? }

??????? private Singleton()
??????? {
??????????? // cannot be created except within this class
??????????? Logger.Log("Constructor invoked.");
??????? }
??? }        

the second version - simple thread-safe

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

??????? public static Singleton Instance
??????? {
??????????? get
??????????? {
??????????????? Logger.Log("Instance called.");
??????????????? lock (padlock) // this lock is used on *every* reference to Singleton
??????????????? {
??????????????????? if (_instance == null)
??????????????????? {
??????????????????????? _instance = new Singleton();
??????????????????? }
??????????????????? return _instance;
??????????????? }
??????????? }
??????? }

??????? private Singleton()
??????? {
??????????? // cannot be created except within this class
??????????? Logger.Log("Constructor invoked.");
??????? }
??? }        

The third version - attempted thread-safety using double-check locking

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

??????? public static Singleton Instance
??????? {
??????????? get
??????????? {
??????????????? Logger.Log("Instance called.");
??????????????? if (_instance == null) // only get a lock if the instance is null
??????????????? {
??????????????????? lock (padlock)
??????????????????? {
??????????????????????? if (_instance == null)
??????????????????????? {
??????????????????????????? _instance = new Singleton();
??????????????????????? }
??????????????????? }
??????????????? }
??????????????? return _instance;
??????????? }
??????? }

??????? private Singleton()
??????? {
??????????? // cannot be created except within this class
??????????? Logger.Log("Constructor invoked.");
??????? }
??? }        

Analysis

We added locking to enforce thread safety, ensuring only one thread at a time can enter the block in which we create the instance of the class. The first approach worked, but the lock applied to every GET request, even though it's only necessary when the instance hasn't yet been created, and that should only happen one time in the entire life of the application, so we're paying this price for the whole life of the application when we only need it once. The subsequent version is better since it uses double?checked locking. However, it also has some issues in that it's complex, it's easy to get wrong

Static Constructors and Singletons

Another approach that doesn't involve locking is to leverage the C# feature of static type construction. C# static constructors run once per app domain, so they are a good tool to consider when it comes to implementing singleton behavior. They are called the first time any static member of a type is referenced. This provides us with some degree of lazy instantiation, although it's not perfect since any reference to any static member, not necessarily our singleton reference, will trigger constructor execution. Make sure you use an explicit static constructor to avoid issues with the C# compiler and beforefieldinit. Essentially, beforefieldinit is a hint the compiler uses to let it know static initializers can be called sooner, and this is the default if the type does not have an explicit static constructor. Adding an explicit static constructor avoids having beforefieldinit applied, which helps make our singleton behavior lazier

the first version for the static constructor

if we read from any other static field or member of this class, we are going to also initialize our singleton instance

public sealed class Singleton
??? {
??????? private static readonly Singleton _instance = new Singleton();

??????? // reading this will initialize the _instance
??????? public static readonly string GREETING = "Hi!";

??????? // Tell C# compiler not to mark type as beforefieldinit
??????? // (https://csharpindepth.com/articles/BeforeFieldInit)
??????? static Singleton()
??????? {
??????? }

??????? public static Singleton Instance
??????? {
??????????? get
??????????? {
??????????????? Logger.Log("Instance called.");
??????????????? return _instance;
??????????? }
??????? }

??????? private Singleton()
??????? {
??????????? // cannot be created except within this class
??????????? Logger.Log("Constructor invoked.");
??????? }
??? }
        

So it's not as lazy as we would like. We can fix this, though, it just requires a little bit of tricky code.

The second version for the static constructor

In this case, we still have a read-only string GREETING, but our static instance now is going to return a nested class and its instance field. If we look here at the nested class, it's the nested class that has a read-only Singleton _instance that it's sharing up to the singleton type that we've wrapped around it. And this type, when it is requested, will only have its initializer called the first time that it's called. Note that there are no other static fields on the nested class, and that's how we get around the issue of accidentally loading it too soon if some other static member on this class is called. Note that we would like to make it so that the only thing that could access that Singleton _instance field is the wrapping class, but we can't mark it as private, so the closest we can get is marking it internal

public sealed class Singleton
??? {
??????? // reading this will initialize the instance
??????? public static readonly string GREETING = "Hi!";
??????? public static Singleton Instance
??????? {
??????????? get
??????????? {
??????????????? Logger.Log("Instance called.");
??????????????? return Nested._instance;
??????????? }
??????? }

??????? private class Nested
??????? {
??????????? // Tell C# compiler not to mark type as beforefieldinit (https://csharpindepth.com/articles/BeforeFieldInit)
??????????? static Nested()
??????????? {
??????????? }
??????????? internal static readonly Singleton _instance = new Singleton();
??????? }

??????? private Singleton()
??????? {
??????????? // cannot be created except within this class
??????????? Logger.Log("Constructor invoked.");
??????? }
??? }        

Analysis

Let's review the static constructor approaches.

  1. They are thread-safe, which is good.
  2. They don't require the use of locks, so they offer good performance characteristics both at application startup and throughout the life of the application. However, the best solution offered so far, the nested class approach,
  3. is fairly complex and non?intuitive (in nested classes ). It's not typically something you would just jump to as your first option when you're going to implement singleton behavior.

Lazy<T> and singleton

Lazy initialization of an object means that its creation is deferred until it is first used. (For this topic, the terms lazy initialization and lazy instantiation are synonymous.) Lazy initialization is primarily used to improve performance, avoid wasteful computation, and reduce program memory requirements

When you create a Lazy<T> type, you specify the type and a function that returns an instance of the type. We can use this to implement the singleton pattern

public sealed class Singleton
??? {
??????? // reading this will initialize the instance
??????? public static readonly Lazy<Singleton> _lazy = new Lazy<Singleton>(() => new Singleton());
??????? public static Singleton Instance
??????? {
??????????? get
??????????? {
??????????????? Logger.Log("Instance called.");
??????????????? return _lazy.Value;
??????????? }
??????? }

??????? private Singleton()
??????? {
??????????? // cannot be created except within this class
??????????? Logger.Log("Constructor invoked.");
??????? }
??? }        

Singleton behavior using containers

  • .NET Core has built?in support for these IoC or DI containers
  • classes will typically request their dependencies in their constructor
  • classes in these applications should follow the explicit dependencies principle by exposing it in the class's constructor
  • container manage instance lifetime

Manage lifetime using a container, not class design

The below three methods define the lifetime of the services

  1. AddTransient: Transient lifetime services are created each time they are requested. This lifetime works best for lightweight, stateless services.
  2. AddScoped: Scoped lifetime services are created once per request
  3. AddSingleton: Singleton lifetime services are created the first time they are requested (or when ConfigureServices is run if you specify an instance there) and then every subsequent request will use the same instance.

Summary

  1. A singleton is designed to only ever have one instance created.
  2. The singleton pattern revolves around making the class itself responsible for enforcing this behavior
  3. it's easy to get this pattern wrong when you're trying to implement it by hand
  4. the Lazy<T> approach that we covered is one of the better ways to apply the pattern
  5. you're working in a framework that supports dependency injection and IoC containers, they are usually the best place to manage the lifetime of the instances of your classes as opposed to putting that responsibility in the classes themselves











要查看或添加评论,请登录

社区洞察

其他会员也浏览了