In most cases, for custom implementation, we are using a Singleton pattern with bugs. C# as an enterprise language provides some advantages and disadvantages for it. Therefore, needs to be careful with custom Singleton implementation.
Singleton Patterns
Singleton patterns are widely used in software development to ensure that a class has only one instance and provides a global point of access to it. However, when dealing with multithreaded environments, subtle bugs can emerge, leading to severe consequences.
In this article, I'll explore a particular implementation (SingletonV1) and uncover some intricate issues it presents in such environments.
Understanding the Problems
- Race Conditions and Time Slicing: In multithreading, the order of execution can be unpredictable, leading to race conditions. To address this, the code uses a critical section (synchronization) to prevent multiple threads from interfering with each other.
- Cacheable Value for CPU Register: The reading of cacheable values for CPU registers can pose problems when threads are invoked simultaneously. While rare, this issue has been observed in languages like Java. Fortunately, the Common Language Runtime (CLR) addresses this concern through a memory fence model.
- Locking Overhead: Double-checking lock is employed to reduce locking overhead, ensuring that only the first thread entering the critical section locks and subsequent threads skip the locking step if the instance already exists.
public sealed class SingletonV1
{
// Sample field of the class
private string _hexCode;
private SingletonV1()
{
// Code to initialize the one Singleton object goes here
_hexCode = "#FFFFFF";
}
private static readonly Object _objLock = new Object();
private static SingletonV1 _instOfSingleton;
public static SingletonV1 GetInstance()
{
if(_instOfSingleton != null)
return _instOfSingleton;
Monitor.Enter(_objLock); // You can use lock instead of it
if(_instOfSingleton == null)
{
// Still not created
_instOfSingleton = new SingletonV1();
}
Monitor.Exit(_objLock);
return _instOfSingleton;
}
}
Hidden bug outside the problems
The code contains a critical flaw related to the compiler's potential reordering of operations during instance creation. Specifically, the compiler might allocate memory, publish the reference, and then call the constructor. In this case, if another thread calls GetInstance() in between publishing and constructor execution, it can use an incomplete object, leading to a hard-to-detect bug!
The solution is volatile
In SingletonV2 class, the code addresses the aforementioned bug by utilizing Volatile.Write(). This ensures that the reference in tempInstance is published into _instOfSingleton only after the constructor has completed execution. This simple yet effective fix prevents other threads from accessing an incomplete Singleton object.
public sealed class SingletonV2
{
// Sample field of the class
private string _hexCode;
private SingletonV2()
{
// Code to initialize the one Singleton object goes here
_hexCode = "#FFFFFF";
}
private static readonly Object _objLock = new Object();
private static SingletonV2 _instOfSingleton;
public static SingletonV2 GetInstance()
{
if(_instOfSingleton != null)
return _instOfSingleton;
Monitor.Enter(_objLock); // You can use lock instead of it
if(_instOfSingleton == null)
{
SingletonV2 tempInstance = new SingletonV2();
// The bug fixed
Volatile.Write(ref _instOfSingleton, tempInstance);
}
Monitor.Exit(_objLock);
return _instOfSingleton;
}
}
Also that bug can be solved by volatile keyword (declaring _instOfSingleton field with volatile keyword). However, this solutions will hurt your performance because the keyword makes all reads volatile, too.
Another solution is Lazy class
Another curious way is to use a Lazy class, which helps reduce duplication, lazy initialization and thread-safe creation of the Singleton instance.
public sealed class SingletonV3
{
// Sample field of the class
private string _hexCode;
private SingletonV3()
{
// Code to initialize the one Singleton object goes here
_hexCode = "#FFFFFF";
}
private static Lazy<SingletonV3> _lazyInstance
= new Lazy<SingletonV3>(InitializeInstance,
LazyThreadSafetyMode.ExecutionAndPublication);
public static SingletonV3 GetInstance()
{
return _lazyInstance.Value;
}
// Just for creating complexy instance
private static SingletonV3 InitializeInstance()
{
//....
return new SingletonV3();
}
}
This approach is more appropriate in terms of tested reliability.
Understanding the complexities of multithreading is crucial for writing robust code. The Singleton pattern, while powerful, requires careful consideration in such environments. By addressing issues like race conditions, cacheable values, locking overhead, and compiler reordering, developers can create thread-safe Singleton implementations.
In simple terms, when using multithreading with Singleton and double-checking, you may get something unexpected. Therefore, in this article, I tried to help you catch that unexpected cases.
Hope you enjoy it!
Top comments (1)
Nice article! Well done!