thread safe

Revisiting the Thread-Safe C++ Singleton

I wrote a long time ago what was basically a note to myself about implementing the Singleton pattern in C++. Here I will update and expand on some of the issues.

Obviously, the code presented here could take the form of a template, but for clarity I'm showing it as an example class named T.

Single-Threaded Implementation

If you know for certain that your singleton will be first accessed in a single-threaded context -- for example, during static initialization when no other threads yet exists -- you can do a simple test for the singleton's existence.

The class has a private static pointer to the singleton instance, and a public static function instance() to get the pointer to the instance of the singleton. In the instance() function, if the singleton does not yet exist, it is created and assigned to the pointer; then the pointer is returned.

We declare T's constructor and destructor private so that a T cannot be directly instantiated.

// T.h:

class T
  {
  public:
    static T* instance();
  private:
    T() {}
    ~T() {}
    static T* smInstance;
  };

// T.cpp:

T* T::smInstance = NULL;

T* T::instance()
  {
  if (smInstance == NULL)
    smInstance = new T();

  return smInstance;
  }

Multi-Thread-Safe Implementation

The problem that arises in a multi-threaded environment is that multiple threads might try to access the singleton at the exact same moment. If the singleton does not yet exist, you need to ensure that two threads don't both create instances of the purported singleton.

To accomplish this, you must have a mutual exclusion lock to protect the singleton creation code from being executed by more than one thread at a time.

In this code, VMutex is the class that implements a mutual exclusion lock, while VMutexLocker is the RAII-style class that automatically handles locking and unlocking in an exception-safe fashion. These are from my Code Vault cross-platform library.

What we've added here is the lock, and the acquisition of the lock before we test and optionally create the instance of the singleton.

// T.h:

class T
  {
  public:
    static T* instance();
  private:
    T() {}
    ~T() {}
    static T* smInstance;
    static VMutex smMutex;
  };

// T.cpp:

T* T::smInstance = NULL;
VMutex T::smMutex;

T* T::instance()
  {
  VMutexLocker lock(&smMutex);
  
  if (smInstance == NULL)
    smInstance = new T();

  return smInstance;
  }

Attempting to Avoid Locking Overhead

That's pretty simple, and it works. However, you might notice that we've incurred the overhead of locking every time someone accesses the instance of the singleton.

There's a well-known pattern to eliminate this overhead, called the double-checked lock. It takes advantage of the fact that we can (apparently) check for null as a quick test before we even bother with locking. Here's how this would look:

T* T::instance()
  {
  if (smInstance == NULL)
    {
    VMutexLocker lock(&smMutex);
  
    if (smInstance == NULL) // double-check
      smInstance = new T();
    }

  return smInstance;
  }

The idea is if we can see that the instance pointer is not null, then there's no need to even enter the synchronized lock and consider creating it.

Once we determine that the instance may need to be created, we enter the locked section. However, we need to re-check for null once we acquire the lock, because "we" may be the thread that lost a race to the lock. That is, another thread may have also checked for null at the same time (we both saw a null pointer) and then it acquired the lock first, created the instance, and released the lock before we were given the lock.

Unfortunately, while this will work reliably on many if not almost all platforms, there is no requirement that the combination of the compiler and the processor's memory model will work as this code intends. It is simply not guaranteed to work.

The Potential Problem

The problem comes from the following statement and the lack of guarantee of the order in which it performs its multiple sub-operations. (The same problem can exist in other languages such as Java and .NET; this is not a C++-specific issue.)

smInstance = new T();

Remember that in our attempt to optimize away unnecessary locking, another thread may be executing the first null check statement (prior to lock acquisition) at the exact same time as our thread is creating and assigning the instance:

if (smInstance == NULL)

Here is the core problem: We have no guarantee that T will be fully constructed before the pointer smInstance is assigned. For example, the compiler is within its rights to generate processor instructions that implement the statement in the following order:

  1. Allocate a memory block sized for T.
  2. Assign the address of the memory block to smInstance
  3. Call T's constructor.

 

(Multi-processor memory architectures can provide a similarly "hostile" environment to our code.)

Consider then what happens if the other thread performs its unsynchronized null pointer test when we are between steps 2 and 3 of the instantiation statement: It will see a non-null value, and will proceed to return the pointer to the raw memory of the not-yet-constructed object.

The Outcome

Fortunately, we can fall back to the original "thread-safe" implementation if we want the instance() function to work with certainty from multiple threads on any platform and compiler.

This leaves you with some choices. If you are sure that the first call to instance() will be made in a single-threaded mode, such as during static initialization, then you don't need the lock. Similarly, if you know that (or can structure your code such that) the instance is created from the main thread before you create other threads, then you don't need the lock. Alternatively, the performance overhead of the lock may well be a non-issue unless you are calling instance() frequently--a classic case of "don't optimize prematurely", where you shouldn't work to eliminate the lock if it isn't a problem to begin with. And of course, if you reference the singleton a lot in one place, you can avoid the cost of checking the lock repeatedly by calling instance() once and then using the object directly, rather than calling instance() repeatedly.

In my Code Vault library, I've taken this two steps further, first by implementing the singleton pattern as a template that is parameterized with the locking requirements, and also allowing the singleton to be registered for deletion during program termination, with rules for whether a deleted singleton can "resurrected" if it is accessed after being deleted.

Reference

C++ and the Perils of Double-Checked Locking

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值