751-C + + design pattern - singleton pattern

Keywords: C++ Singleton pattern

Design pattern concept

In brief, the design pattern means that when solving a certain kind of problem scenario, there is an established and excellent code framework that can be used directly. Compared with the problem-solving method we have found out ourselves, it has the following advantages:
1. The code is easier to maintain, and the readability, reusability, portability and robustness of the code will be better
2. When the original requirements of software are changed or new requirements are added, the application of reasonable design mode can achieve the "open close principle" of software design requirements, that is, it is closed to modification and open to expansion, so that the modification of original functions and the expansion of new functions of software are very flexible
3. The choice of reasonable design mode will make the software design more modular and actively achieve the fundamental principle of "high cohesion and low coupling" followed by software design

Introduction to singleton mode

The singleton mode means that no matter how you get it, you can always get the only instance object of this type. Therefore, to design a singleton, you must meet the following three conditions:
1. The constructor is privatized, so that the user cannot define the object of this type arbitrarily
2. Define unique objects of this type
3. Return a unique object instance through a static static member method

Hungry man single case mode

The following code demonstrates a starving singleton mode:

#include<iostream>
using namespace std;
class CSingleton
{
public:
	static CSingleton* getInstance()
	{
		return &single;
	}
private:
	static CSingleton single;
	CSingleton() { cout << "CSingleton()" << endl; }
	~CSingleton() { cout << "~CSingleton()" << endl; }
	CSingleton(const CSingleton&);//Prevent external use of copy constructs to generate new objects, such as CSingleton s = *p1 below;
};
CSingleton CSingleton::single;

int main()
{
	CSingleton* p1 = CSingleton::getInstance();
	CSingleton* p2 = CSingleton::getInstance();
	CSingleton* p3 = CSingleton::getInstance();
	cout << p1 << " " << p2 << " " << p3 << endl;
	return 0;
}


It can be seen that the CSingleton object obtained three times is the same object instance, which is a starving singleton pattern.
Hungry Han singleton mode, as the name suggests, is that the object is instantiated when the program starts, and the instantiation is not delayed until the object is used for the first time; If it is not used in the running process, the instance object is wasted.

Lazy singleton mode

Let's continue to demonstrate a lazy singleton mode:

#include<iostream>
using namespace std;
class CSingleton
{
public:
	static CSingleton* getInstance()
	{
		if (nullptr == single)
		{
			single = new CSingleton();
		}
		return single;
	}
private:
	static CSingleton* single;
	CSingleton() { cout << "CSingleton()" << endl; }
	~CSingleton() { cout << "~CSingleton()" << endl; }
	CSingleton(const CSingleton&);
};
CSingleton* CSingleton::single = nullptr;

int main()
{
	CSingleton* p1 = CSingleton::getInstance();
	CSingleton* p2 = CSingleton::getInstance();
	CSingleton* p3 = CSingleton::getInstance();
	cout << p1 << " " << p2 << " " << p3 << endl;
	return 0;
}


It meets the requirements of the singleton mode. The same object is obtained three times. When the program starts, only null values are initialized for the single pointer. When the getInstance function is called for the first time, the object is instantiated because the single pointer is nullptr, so it is a lazy singleton mode.

Therefore, the lazy singleton pattern, as its name suggests, is to delay the instantiation of an object until it is used for the first time.

Let's see, the objects from new above have never seen delete, which is not good. Of course, there is new without delete, which is not paired! Others say that whatever it is, at the end of the current process, the system will recycle all the resources allocated to it, including unrecovered memory. However, as a C + + developer, we must consider the allocation and recycling of resources clearly and not be confused. How do you feel about the following modifications:

int main()
{
	CSingleton *p1 = CSingleton::getInstance();
	CSingleton *p2 = CSingleton::getInstance();
	CSingleton *p3 = CSingleton::getInstance();
	cout << p1 << " " << p2 << " " << p3 << endl;
	delete p1;//Here, delete the new object, destruct the object and free the memory on the heap
	return 0;
}

How uncomfortable is this method? First, if the resource is released to the user, it will inevitably forget to write delete, or delete multiple times to release the wild pointer. Therefore, the above method of releasing single instance object resources is not good enough. We use the feature that static static objects automatically parse at the end of the program to give the following code for releasing resources, It must be better than the above method. The code is as follows:

#include<iostream>
using namespace std;
class CSingleton
{
public:
	static CSingleton* getInstance()
	{
		if (nullptr == single)
		{
			single = new CSingleton();
		}
		return single;
	}
private:
	static CSingleton* single;
	CSingleton() { cout << "CSingleton()" << endl; }
	~CSingleton() { cout << "~CSingleton()" << endl; }
	CSingleton(const CSingleton&);

	//Define a nested class and automatically release the resources of the outer class in the destructor of the class
	class CRelease
	{
	public:
		~CRelease() { delete single; }
	};
	//Through the feature that the static object is automatically destructed at the end of the program, the object resources of the outer class are released
	static CRelease release;
};
CSingleton* CSingleton::single = nullptr;
CSingleton::CRelease CSingleton::release;

int main()
{
	CSingleton* p1 = CSingleton::getInstance();
	CSingleton* p2 = CSingleton::getInstance();
	CSingleton* p3 = CSingleton::getInstance();
	cout << p1 << " " << p2 << " " << p3 << endl;
	return 0;
}


The object instance has been destructed normally and memory is released.

Thread safe singleton mode

When developing server programs, multithreading is often used. Multithreading should consider the thread safety characteristics of the code and should not allow the code to have race conditions in the multithreaded environment. Otherwise, it is necessary to carry out thread mutually exclusive operation. Let's consider whether the above two singleton modes are thread safe singleton modes if they are used in the multithreaded environment.

1. Thread safety features of starving singleton mode
In hungry Han singleton mode, the singleton object is defined as a static object. It is initialized before the main function runs when the program starts. Therefore, there is no thread safety problem and can be safely used in a multithreaded environment.

2. Thread safety features of lazy singleton mode
Lazy singleton mode, the method of obtaining singleton object is as follows:

static CSingleton* getInstance()
{
	if (nullptr == single)
	{
		single = new CSingleton();
	}
	return single;
}

Obviously, this getInstance is a non reentrant function, that is, when it is executed in a multithreaded environment, there will be race conditions. First, find out the code, single = new CSingleton(), which will do three things, open up memory, call the constructor and assign a value to the single pointer. Then, in a multithreaded environment, the following problems may occur:

  1. Thread A calls the getInstance function first. Since single is nullptr, it enters the if statement
  2. The new operation opens up memory first. At this time, the CPU time slice of thread A is up, and switch to thread B
  3. Since single is nullptr, thread B also enters the if statement and starts the new operation

Obviously, the above two threads have entered the if statement and tried to create a new object, which does not conform to the design of singleton mode. How should we deal with it? By the way, the getInstance function should be locked internally and mutually exclusive between processes. The mutex mutex operation method provided in pthread Library under Linux system is introduced here. The code is as follows:

#include <iostream>
#include <pthread.h>
using namespace std;

class CSingleton
{
public:
	static CSingleton* getInstance()
	{
		//Get mutex
		pthread_mutex_lock(&mutex);
		if (nullptr == single)
		{
			single = new CSingleton();
		}
		//Release mutex
		pthread_mutex_unlock(&mutex);
		return single;
	}
private:
	static CSingleton* single;
	CSingleton() { cout << "CSingleton()" << endl; }
	~CSingleton()
	{
		pthread_mutex_destroy(&mutex); // Release lock
		cout << "~CSingleton()" << endl;
	}
	CSingleton(const CSingleton&);

	class CRelease
	{
	public:
		~CRelease() { delete single; }
	};
	static CRelease release;

	//Define mutex between threads
	static pthread_mutex_t mutex;
};
CSingleton* CSingleton::single = nullptr;
CSingleton::CRelease CSingleton::release;
//Initialization of mutex
pthread_mutex_t CSingleton::mutex = PTHREAD_MUTEX_INITIALIZER;

int main()
{
	CSingleton* p1 = CSingleton::getInstance();
	CSingleton* p2 = CSingleton::getInstance();
	CSingleton* p3 = CSingleton::getInstance();
	return 0;
}

Not only meet the efficiency, but also meet the thread safety, kill two birds with one stone! Since you are learning C + +, you might as well encapsulate the mutex into a class to make it more OOP. The code is as follows:

#include <iostream>
#include <pthread.h>
using namespace std;

//Encapsulation of mutex operations
class CMutex
{
public:
	CMutex() { pthread_mutex_init(&mutex, NULL); }  // Initialization lock
	~CMutex() { pthread_mutex_destroy(&mutex); }  // Destroy lock 
	void lock() { pthread_mutex_lock(&mutex); }  // Acquire lock
	void unlock() { pthread_mutex_unlock(&mutex); }  // Release lock
private:
	pthread_mutex_t mutex;
};

class CSingleton
{
public:
	static CSingleton* getInstance()
	{
		if (nullptr == single)
		{
			//Get mutex
			mutex.lock();
			/*
			Here you need to add another if judgment, otherwise when two
			If all threads enter here, the new object will be created many times, which does not meet the requirements
			Involvement of singleton mode
			*/
			if (nullptr == single)
			{
				single = new CSingleton();
			}
			//Release mutex
			mutex.unlock();
		}

		return single;
	}
private:
	static CSingleton* single;
	CSingleton() { cout << "CSingleton()" << endl; }
	~CSingleton() { cout << "~CSingleton()" << endl; }
	CSingleton(const CSingleton&);

	class CRelease
	{
	public:
		~CRelease() { delete single; }
	};
	static CRelease release;

	//Static mutex between threads
	static CMutex mutex;
};
CSingleton* CSingleton::single = nullptr;
CSingleton::CRelease CSingleton::release;
//Define mutex static objects
CMutex CSingleton::mutex;

int main()
{
	CSingleton* p1 = CSingleton::getInstance();
	CSingleton* p2 = CSingleton::getInstance();
	CSingleton* p3 = CSingleton::getInstance();
	return 0;
}

Problem thinking

Please consider whether the following lazy singleton mode is thread safe. The code is as follows:

#include <iostream>
using namespace std;

class CSingleton
{
public:
	static CSingleton* getInstance()
	{
		static CSingleton single;//Lazy singleton mode, which defines a unique object instance
		return &single;
	}
private:
	static CSingleton* single;
	CSingleton() { cout << "CSingleton()" << endl; }
	~CSingleton() { cout << "~CSingleton()" << endl; }
	CSingleton(const CSingleton&);
};
int main()
{
	CSingleton* p1 = CSingleton::getInstance();
	CSingleton* p2 = CSingleton::getInstance();
	CSingleton* p3 = CSingleton::getInstance();
	return 0;
}

When the above singleton mode is used in A multi-threaded environment, will this happen? When thread A calls the getInstance function for the first time, the single object is initialized for the first time. At this time, thread B also calls the getInstance function. Will it also initialize the single object, because thread A does not initialize the single object at this time?

In the Linux environment, compile the above code through g + +, and the command is as follows:
g++ -o main main.cpp -g
Generate the executable file main, debug with gdb, to the getInstance function, and print the assembly instruction of the function, as follows:

It can be seen that for the initialization of static static local variables, the compiler will automatically lock and unlock its initialization to make the initialization of static local variables a thread safe operation. There is no need to worry that multiple threads will initialize static local variables. Therefore, the lazy singleton mode above is a thread safe singleton mode!

Posted by biffta on Tue, 02 Nov 2021 08:53:12 -0700