Problem-Solving with the Singleton Design Pattern: A Before-and-After Code Analysis

I recently refreshed my knowledge on several popular design patterns, and the Singleton Design Pattern was one of them. I’d like to share some insights about it, along with code examples. If you’re trying to understand this pattern and stumbled upon my article, I hope it can serve as a helpful resource for you

By textbook definition, the Singleton Design Pattern is employed when we need a single object for purposes like caching, thread pools, etc. The initial description can be confusing, so let me simplify it: it’s all about efficiency and resource management.

If I had to explain the Singleton Design Pattern to a 6-year-old, I would say:

Imagine you have a big toy box that takes a long time to open because it’s locked with many locks. Instead of unlocking it every time you want a toy, the Singleton is like opening it once in the morning and then keeping it open all day so you can quickly grab toys whenever you want. It saves you a lot of time!

Singleton Design Pattern

Now let's move to a simple code example. Suppose I have a Logger class, I called it NonSingletonLogger /This class contains a method called LogMessage that logs any text passed to it as a parameter into a file.

public class NonSingletonLogger
{
    private readonly string path = "D:\\Log\\MyFile.txt";

    public NonSingletonLogger()
    {
         Thread.Sleep(2000);//take a lot of time to fetches configuration data, establishes database connections etc 

    }
    public void LogMessage(string message)
    {
        try
        {
            // Ensure directory exists
            Directory.CreateDirectory(Path.GetDirectoryName(path));

            // Append text to the file
            File.AppendAllText(path, message + Environment.NewLine);
        }
        catch (Exception ex)
        {
            Console.WriteLine("An error occurred: " + ex.Message);
        }
    }
}

As you can see in the constructor, every time we create an instance of this class, it takes significant time to fetch configuration data, establish database connections, and perform other initializations. I’ve simulated this resource-intensive process by introducing a 2-second sleep in the constructor.

Given that our Logger class is immensely popular, it’s frequently invoked from various parts of the code. To replicate this behavior, I’ve created two methods, Method1() and Method2(), both of which call our in-demand Logger class.

public class Program
{
    static void Main(string[] args)
    {
        Stopwatch stopwatch = new Stopwatch();

        stopwatch.Start(); // Start the stopwatch
        Method1();
        Method2();
        stopwatch.Stop(); // Stop the stopwatch

        Console.WriteLine($"The total time spend to complete Method1 and Method2 under non singleton " + $"pattern are: {stopwatch.Elapsed.TotalSeconds} seconds");
        Console.ReadLine();
    }

    static void Method1()
    {
        NonSingletonLogger nonSingletonLogger = new NonSingletonLogger();
        nonSingletonLogger.LogMessage("Some text...");
    }

    static void Method2()
    {
        NonSingletonLogger nonSingletonLogger = new NonSingletonLogger();
        nonSingletonLogger.LogMessage("Some text...");
    }
}

As you can see in the code sample, in both method, the keyword ‘new’ is used every time the Logger class is used. This means every time a new instance is created, the constructor have to perform everything it needs to perform (in our case it sleeps 2 seconds) to instantiate.I introduced a stopwatch to measure the time required to complete both methods, and the result is around 4 seconds.

Singleton Design Pattern

Now, let me demonstrate how we can achieve better performance using the Singleton Design Pattern. The code is as follows

public sealed class SingletonLogger
{
    private readonly string path = "D:\\Log\\MyFile.txt";
    private static SingletonLogger instance;

    private SingletonLogger()
    {
        Thread.Sleep(2000);  
    }

    public static SingletonLogger GetInstance()
    {
        if (instance == null)
        {     instance = new SingletonLogger();
             
        }
        return instance;

    }

    public void LogMessage(string message)
//{.... the rest of the codes

Pay attention to few things

  1. Sealed access modifier. Using Sealed is not mandatory when creating a Singleton Design Pattern, but it is recommended. This modifier prevents inheritance, which could lead to unexpected behavior. In some scenarios, it can enhance performance. Moreover, it signals to other developers that this class shouldn’t be subclassed.
  2. Private constructor. A private constructor ensures that no other classes or methods can instantiate this class using the ‘new’ keyword.
  3. GetInstance method. This method checks if the instance variable is null. If it is, a new instance is created; otherwise, the existing one is returned. This ensures that our class remains the sole instance throughout the system.

As a result, other classes or methods aiming to use our Singleton Logger class won’t be able to utilize the ‘new’ keyword. Instead, they should call SingletonLogger.GetInstance() to access the logger instance.

static void Method1()
{
    SingletonLogger singletonLogger = SingletonLogger.GetInstance();
    singletonLogger.LogMessage("Some text...");
}

To demonstrate the utility of the Singleton Design Pattern, especially in preventing resource-intensive behaviors from recurring, I invoked both methods five times more.

stopwatch.Start(); // Start the stopwatch
Method1();Method1();Method1();Method1(); Method1();
Method2();Method2();Method2();Method2(); Method2();
stopwatch.Stop(); // Stop the stopwatch

The result took only around 2 seconds because the resource-intensive part (within the constructor) executed just once.

Singleton Design Pattern

However, in the real world, a system is seldom used by just one person. If multiple requests try to access the Logger class-implying a multi-threaded scenario-can our Singleton Design Pattern still perform efficiently? Let’s test this out.

I created a Task array of size 3 and iterated over it using a for loop. Within the loop, I call Method1(). This simulates three threads calling Method1(), which essentially means three threads are attempting to access our SingletonLogger class.

static void Main(string[] args)
{
    Task[] tasks = new Task[3];
    for (int i = 0; i < 3; i++)
    {
        int writer = i+1;
        tasks[i] = Task.Run(() => Method1(writer));
    }

    Task.WaitAll(tasks);

    Console.WriteLine($"Program ends here");
    Console.ReadLine();
}

static void Method1(int writer)
{
    SingletonLogger singletonLogger = SingletonLogger.GetInstance();
    singletonLogger.LogMessage($"Some text from writer {writer}...");
}

Then an error occurred:

Singleton Design Pattern

The error arises due to a ‘race condition’, where multiple threads attempt to write to the file simultaneously. This highlights an important concept when working with the Singleton Design Pattern: Thread-Safety.

Before I show how to deal with the race condition, let us revisit our Singleton Constructor. Could multiple threads create several instances? Even though we’ve implemented a check with if (instance == null) before creating a new instance, what if multiple threads evaluate this condition simultaneously and find it true? Could this lead to the creation of multiple instances?

Let’s try by putting a Console.WriteLine message in the constructor, and run the main program again in a multi-threaded environment.

private SingletonLogger()
{
    Thread.Sleep(2000);
    //put a WriteLine message, so we can tell how many times this constructor being called
    Console.WriteLine( "Instance Created");
    
}

static void Method1()
{
    SingletonLogger singletonLogger = SingletonLogger.GetInstance();
  //  singletonLogger.LogMessage("Some text...");
  //we temporary comment out the log message function
  //just want to see how many times the constructor being called
}

 

//in main function
static void Main(string[] args)
{  //the main function still run 3 thread
    Task[] tasks = new Task[3];
    for (int i = 0; i < 3; i++)
    {
        int writer = i+1;
        tasks[i] = Task.Run(() => Method1(writer));
    }

    Task.WaitAll(tasks);

    Console.WriteLine($"Program ends here");
    Console.ReadLine();
}

And you can see the results, it prints the constructor’s message 3 times!

Singleton Design Pattern

This indicates a flaw in our Singleton Design Pattern. In a multi-threaded scenario, we inadvertently created three instances. Imagine the resources wasted if every constructor call consumed significant resources.

So, in order to address this problem, we need to use lock. The code implementation is like this:

private readonly string path = "D:\\Log\\MyFile.txt";
private static SingletonLogger instance;
private static readonly object syncLock = new object();

public static SingletonLogger GetInstance()
{
    if (instance == null)
    {
        lock (syncLock)
        {
            if (instance == null)
                instance = new SingletonLogger();
        }
    }
    return instance;
}

Lock is a C# statement ensuring that a specific block of code cannot be executed by more than one thread simultaneously.

Referring to the above code, we’ve created an object variable named syncLock. This variable is used in conjunction with the lock statement. When a thread encounters a lock statement, it attempts to obtain an exclusive lock on the specified object. If no other thread holds the lock, it gains it and proceeds to execute the code block.

After incorporating the lock into the code and rerunning it, only a single instance was created this time.

Singleton Design Pattern

Just to let you know, this method of using lock is called Double-Checked Locking pattern, since it checks twice whether the instance is null. So you learn another term in Singleton Design Pattern that can answer in an interview section.

Okay, now we have solve the only single instance can be created, how about the ‘race condition’?

The solution is pretty much the same, we also apply the lock in the LogMessage method, like this:

public sealed class SingletonLogger
{
    //...
    private static readonly object syncLock = new object();
    private static readonly object fileLock = new object();
    //... 

    public void LogMessage(string message)
    {
        lock (fileLock)
        {
            try
            {
                // Ensure directory exists
                Directory.CreateDirectory(Path.GetDirectoryName(path));

                // Append text to the file
                File.AppendAllText(path, message + Environment.NewLine);
            }
            catch (Exception ex)
            {
                Console.WriteLine("An error occurred: " + ex.Message);
            }
        }
    }
}

We create another object variable called fileLock, and use it to determine whether the method is exclusive lock when it is access by thread. Please take note that make sure you use different object variable , if you have different method that also requiring to lock checks. In this case, I used separate variables for the constructor and the LogMessage method.

Upon execution, there were no errors, and the log messages were successfully written to the text file

Singleton Design Pattern

Within the topic of the Singleton Design Pattern, there are two strategies: Eager Initialization and Lazy Initialization. However, to keep this article concise, I’ve decided to address these strategies in a separate follow-up piece.

Part 2: Singleton Design Pattern: Eager And Lazy Initialization With Code Example

You may have the source code from my Github.