Exploring thread-safe, single-locking singletons

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.

How to do it…

To explore thread-safe, single-locking singletons, execute the following steps:

  1. Define a singleton class with a shared instance.
  2. Define a thread-local initialized variable.
  3. In the get function, if it is not initialized, enter a synchronized block that creates the instance, if necessary.
  4. Set the initialized value to true.
  5. Return the shared instance.

The following is the code:

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.

How it works…

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.

See also

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.79.20