Lazy initialization in a multithreaded environment poses an interesting problem: how do you avoid a race condition where the variable is initialized by two different threads, which would defeat the purpose of using a singleton in the first place?
The most straightforward solution is to always use synchronization when getting the singleton instance. This correctly solves the race condition, but comes with a significant performance cost. Clever programmers devised a way to avoid synchronization with a double-checked locking mechanism, checking if the object needs initialization both before and after entering a synchronized block. This has good performance, but has a major problem: it is buggy. Optimizing compilers or different memory models across platforms meant that the double check cannot be relied upon to do the right thing in all cases.
D's built-in thread-local variables offer an elegant solution to the problem that brings the correctness of the straightforward solution and the performance of the double-checked solution together, without the need for complicated code.
To explore thread-safe, single-locking singletons, execute the following steps:
shared
instance.get
function, if it is not initialized, enter a synchronized block that creates the instance, if necessary.initialized
value to true
.shared
instance.class ThreadLocalSingleton { private static bool initialized; private static shared(ThreadLocalSingleton) instance; static shared(ThreadLocalSingleton) getInstance() { if(!initialized) { synchronized(ThreadLocalSingleton.classinfo) { if(instance is null) { instance = new shared ThreadLocalSingleton(); } } initialized = true; } return instance; } int count = 0; void foo() shared { import std.stdio; count++; writeln("foo ", count); } } void useSingleton(shared(ThreadLocalSingleton) s) { s.foo(); } void main() { auto i = ThreadLocalSingleton.getInstance(); useSingleton(i); // you can also spawn new threads that use the singleton }
The program will compile and run, printing a count. You can add additional threads and see that it continues to work efficiently.
Since locking is expensive, avoiding synchronized blocks can often improve performance. Using D's built-in thread-local storage, we can efficiently bypass all but the first lock for any thread, without using bug-prone double-locking mechanisms.
3.15.31.22