How to write a thread-safe singleton pattern in C++?

How to write a thread-safe singleton pattern?

A simple implementation of the singleton pattern

The singleton pattern is probably one of the most widely circulated design patterns. A simple implementation code probably looks like this:

class singleton
{
public:
	static singleton* instance()
	{
		if (inst_ != nullptr) { 
			inst_ = new singleton();
		}
		return inst_;
	}
private:
	singleton(){}
	static singleton* inst_;
};

singleton* singleton::inst_ = nullptr;

This code is completely fine in a single-threaded environment, but in a multi-threaded world, the situation is a little different. Consider the following order of execution:

  1. After thread 1 executes if (inst_ != nullptr), it hangs;
  2. Thread 2 executes the instance function: since inst_ has not been assigned, the program will inst_ = new singleton () statement;
  3. Thread 1 resumes, the inst_ = new singleton() statement is executed again, and the singleton handle is created multiple times.

Therefore, such an implementation is thread-unsafe.

Problematic Double Checked Locks

To solve the problem of multi-threading, the most common method is to add locks. Then it is easy to get the following implementation version:

class singleton
{
public:
	static singleton* instance()
	{
		guard<mutex> lock{ mut_ };
		if (inst_ != nullptr) {
			inst_ = new singleton();
		}
		return inst_;
	}
private:
	singleton(){}
	static singleton* inst_;
	static mutex mut_;
};

singleton* singleton::inst_ = nullptr;
mutex singleton::mut_;

This problem is solved, but the performance is not so satisfactory. After all, every time the instance is used, there is an additional cost of locking and unlocking. More importantly, this lock is not needed every time! In fact, we only need to lock when creating a singleton instance, and we don't need to lock at all when using it later. Therefore, someone proposed a way of writing a double detection lock:

...
	static singleton* instance()
	{
		if (inst_ != nullptr) {
			guard<mutex> lock{ mut_ };
			if (inst_ != nullptr) {
				inst_ = new singleton();
			}
		}
		return inst_;
	}
...

Let's first judge whether inst_ has been initialized, and if not, proceed to the lock initialization process. In this way, although the code looks a bit weird, it seems that it does achieve the purpose of introducing lock overhead only when creating a singleton. Unfortunately, this approach is problematic. Scott Meyers and Andrei Alexandrescu  discussed this issue in great detail in the article C++ and the Perils of Double-Checked Locking . We will only make a simple explanation here. The problem lies in:

	inst_ = new singleton();

this line. This code is not atomic, it is usually divided into the following three steps:

  1. Call operator new to allocate memory space for the singleton object;
  2. Call the constructor of singleton on the allocated memory space;
  3. Assign the allocated memory space address to inst_.

If the program can execute the code strictly according to the steps of 1-->2-->3, then there is no problem with the above method, but the actual situation is not the case. The optimized rearrangement of instructions by the compiler and the out-of-order execution of CPU instructions (for specific examples, please refer to "[Multi-threading Things] Is the execution order of multi-threading as you expected?" ) It is possible to make step 3 execute earlier than step 3 2. Consider the following order of execution:

  1. Thread 1 is executed in the order of steps 1-->3-->2, and is suspended after executing steps 1 and 3;
  2. Thread 2 executes the instance function to obtain a singleton handle for further operations.

Since inst_ has been assigned in thread 1, a non-empty inst_ instance can be obtained in thread 2 and the operation continues. But in fact, the creation of the singleton object has not been completed, and any operation at this time is undefined.

A workaround in modern C++

In modern C++, we can implement a thread-safe and efficient singleton mode through the following methods.

Using memory order constraints in modern C++

Modern C++ specifies 6 memory execution sequences. Reasonable use of memory order restrictions can avoid code instruction rearrangement. A possible implementation is as follows:

class singleton {
public:
	static singleton* instance()
	{
		singleton* ptr = inst_.load(memory_order_acquire);
		if (ptr == nullptr) {
			lock_guard<mutex> lock{ mut_ };
			ptr = inst_.load(memory_order_relaxed);
			if (ptr == nullptr) {
				ptr = new singleton();
				inst_.store(ptr, memory_order_release);
			}
		}
	
		return inst_;
	}
private:
	singleton(){};
	static mutex mut_;
	static atomic<singleton*> inst_;
};

mutex singleton::mut_;
atomic<singleton*> singleton::inst_;

Take a look at the assembly code:

As you can see, the compiler has inserted the necessary statements for us to ensure the execution order of the instructions.

Using the call_once method in modern C++

call_once is also a new feature introduced in modern C++, which can ensure that a function is only executed once. The code implementation using call_once is as follows:

class singleton
{
public:
	static singleton* instance()
	{
		if (inst_ != nullptr) {
			call_once(flag_, create_instance);
		}
		return inst_;
	}
private:
	singleton(){}
	static void create_instance()
	{
		inst_ = new singleton();
	}
	static singleton* inst_;
	static once_flag flag_;
};

singleton* singleton::inst_ = nullptr;
once_flag singleton::flag_;

Take a look at the assembly code:

It can be seen that the program finally calls __gthrw_pthread_once to ensure that the function is only executed once.

use static local variables

Now C++ has the following regulations on the initialization order of variables:

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

So we can simply use a static local variable to implement the thread-safe singleton pattern:

class singleton
{
public:
	static singleton* instance()
	{
		static singleton inst_;
		return &inst_;
	}
private:
	singleton(){}
};

Take a look at the assembly code:

It can be seen that the compiler has automatically inserted relevant code for us to ensure the multi-thread safety of static local variable initialization.

The full text is over.

Guess you like

Origin blog.csdn.net/weixin_47367099/article/details/127458787