Singleton mode from simple to deep (C# version)

Posted by bentobenji on Wed, 13 Oct 2021 02:06:23 +0200

Singleton mode from simple to deep (C# version)

Sometimes, we want a class to have only one instance. The benefits are:
1. Data sharing can be realized
2. Avoid creating and destroying a large number of instances to improve performance

In order to implement the singleton mode, the usual methods are:
1. Privatize the constructor to avoid external direct new objects
2. Provide an external method to return a singleton object instance

The simplest code looks like this:

class SingleTest
{
    private static SingleTest singleTest;
    public int Cnt { get; private set; }
    private SingleTest() { }
    public static SingleTest GetSingleObject()
    {
        if(singleTest is null)
        {
            singleTest = new SingleTest();
            Console.WriteLine("Instance created");
        }
        return singleTest;
    }
    public void Increase()
    {
        Cnt++;
    }
}

In this way, the external can obtain a single instance through a static method, and the instance obtained multiple times is actually the same instance

static void Main(string[] args)
{
	SingleTest singleTest = SingleTest.GetSingleObject();
	SingleTest singleTest2 = SingleTest.GetSingleObject();
	Console.WriteLine($"singleTest == singleTest2 ? {singleTest == singleTest2}"); //True
	
	singleTest2.Increase();
	Console.WriteLine(singleTest.Cnt); // 1
}

The implementation of the above singleton mode is called lazy. The so-called lazy means that an instance can be created only when the GetSingleObject() method is called. It is impossible to create an instance without calling the GetSingleObject() method

The above code is thread unsafe. If multiple threads call GetSingleObject() method at the same time, multiple instances will be generated

class Program
{
    private static object lockObject = new();
    static void Main(string[] args)
    {
        List<Task> tasks = new();

        for (int i = 0; i < 100; i++)
        {
            tasks.Add(Task.Run(() =>
            {
                SingleTest singleTest = SingleTest.GetSingleObject();
                lock (lockObject)
                {
                    singleTest.Increase();
                }
            }));
        }
        Task.WaitAll(tasks.ToArray());

        SingleTest singleTest = SingleTest.GetSingleObject();
        Console.WriteLine(singleTest.Cnt);
    }
}

Running the above code, it is found that "instance created" has been printed many times

To solve the above problems, you can use double check locking, add a property for locking, and modify the GetSingleObject method

private static object lockObject = new();
public static SingleTest GetSingleObject()
{
    if(singleTest is null)
    {
        lock (lockObject)
        {
            if (singleTest is null)
            {
                singleTest = new SingleTest();
                Console.WriteLine("Instance created");
            }
        }
    }
    return singleTest;
}

lock ensures that only one thread can enter at the same time, so that multiple singletons will not be created
The if judgment outside lock is to improve efficiency, because if this if is removed, all threads need to wait for lockObject lock, but such waiting is not necessarily necessary, because waiting for lock is not required at all when singleTest is not null, so this if statement is added to improve efficiency
The if judgment inside the lock is obviously necessary, so it is not necessary for the thread to create an instance once it obtains the lock. If it is removed, it will still cause the problem of creating multiple instances

The complete code of singleton mode (lazy thread safety) is as follows:

class SingleTest
{
    private static SingleTest singleTest;
    private static object lockObject = new();
    public int Cnt { get; private set; }
    private SingleTest() { }
    public static SingleTest GetSingleObject()
    {
        if(singleTest is null)
        {
            lock (lockObject)
            {
                if (singleTest is null)
                {
                    singleTest = new SingleTest();
                    Console.WriteLine("Instance created");
                }
            }
        }
        return singleTest;
    }
    public void Increase()
    {
        Cnt++;
    }
}

There is also a single instance mode called hungry Han style. The principle is to use the property that the static construction method is called only once or the static attribute is assigned once during initialization to ensure that there is only one instance

1. Use the property that the static construction method is called only once to realize the hungry Han style singleton mode

class SingleTest
{
    private static SingleTest singleTest;
    public int Cnt { get; private set; }
    static SingleTest()
    {
        singleTest = new SingleTest();
        Console.WriteLine("Instance created");
    }
    public static SingleTest GetSingleObject()
    {
        return singleTest;
    }
    public void Increase()
    {
        Cnt++;
    }
}

2. Use the property that the static attribute is assigned once during initialization to realize the hungry Han style singleton mode

class SingleTest
{
    private static SingleTest singleTest = new();
    public int Cnt { get; private set; }
    private SingleTest()
    {
    }
    public static SingleTest GetSingleObject()
    {
        return singleTest;
    }
    public void Increase()
    {
        Cnt++;
    }
}

The so-called "hungry man" single instance mode means that even if the GetSingleObject method is not called, the instance will be created during SingleTest initialization, showing a feeling of eagerness. Therefore, it is called "hungry man"
Obviously, starving singleton mode is thread safe

Topics: C# Design Pattern Singleton pattern