C++ Design Pattern Creational Singleton Pattern

I. Overview

        The singleton pattern, also known as the single state pattern, is a creational pattern used to create a class that can only produce one object instance. For example, there is only one sound management system, one configuration system, one file management system, one log system, etc. in the project, and even if the entire Windows operating system is regarded as one project, then there is only one task manager window, etc. in it. The implementation intention of introducing the singleton mode: ensure that only one instance of a class exists, and provide a global method that can access the instance.

2. Classification of singleton mode

1. Lazy Mode

1) Code example

class CSingletonImpl
{
public:
    static CSingletonImpl* GetInstance()
    {
        if (m_pInstance == nullptr)
        {
            m_pInstance = new CSingletonImpl;
        }

        return m_pInstance;
    }
private:
    CSingletonImpl(){};
    ~CSingletonImpl(){};
    CSingletonImpl(const CSingletonImpl& the);
    CSingletonImpl& operator=(const CSingletonImpl& other);
private:
    static CSingletonImpl* m_pInstance;
};

CSingletonImpl*CSingletonImpl::m_pInstance = nullptr;

2) Description

In order to prevent multi-object problems, the singleton mode sets the constructor, destructor, copy constructor, and assignment operator functions as private, and sets the public unique interface method to create objects, and defines class static pointers. This is the general approach, so what could be wrong? If it is used in a single thread, there is no problem, but it may cause problems when used in multiple threads. If multiple threads may cause multiple objects to be generated due to operating system time slice scheduling issues, the solution to this problem is to use GetInstance( ) member function shackles.

Sample code:

Add private member variables: static std::mutex m_mutex;

static CSingletonImpl* GetInstance()
{

 m_mutex.lock();
 if (m_pInstance == nullptr)
 {
  m_pInstance = new CSingletonImpl;
 }
 m_mutex.unlock();

 return m_pInstance;
}

Is there any problem with adding the above code? Haha, not yet. Although there is no problem in code logic and thread safety is achieved by locking the interface function, there is a big problem in terms of execution efficiency. When the program is running, GetInstance() may be frequently called by multiple threads, and each call will go through the process of locking and unlocking, which will seriously affect the efficiency of program execution, and the locking mechanism is only meaningful for the first creation of objects. Once an object is created, it becomes a read-only object. In multi-threading, locking access to a read-only object is not only expensive, but also meaningless. So how to solve this problem? That is the double locking mechanism, based on this mechanism function implementation code:

    static CSingletonImpl* GetInstance()
    {
        if (m_pInstance == nullptr)
        {
            std::lock_guard<std::mutex> siguard(si_mutex);
            if (m_pInstance == nullptr)
            {
                m_pInstance = new CSingletonImpl;
            }
        }
        return m_pInstance;
    }

The above-mentioned double locking mechanism looks perfect, but in fact there are potential problems. The reordering of memory access leads to the failure of double locking. The recommended method is some features of the new C++11 standard. The sample code is as follows:

#include <mutex>
#include <atomic>

//通过原子变量解决双重锁定底层问题(load,store)
class CSingletonImpl
{
public:
    static CSingletonImpl* GetInstance()
    {
        CSingletonImpl* task = m_taskQ.load(std::memory_order_relaxed); 
        std::atomic_thread_fence(std::memory_order_acquire);
        if (task == nullptr)
        {
            std::lock_guard<std::m_mutex> lock(m_mutex);
            task = m_taskQ.load(std::memory_order_relaxed); 
            if (task == nullptr)
            {
                task = new CSingletonImpl;
                std::atomic_thread_fence(std::memory_order_release);
                m_taskQ.store(task, std::memory_order_relaxed);
            }
        }
        return task;
    }
private:
    CSingletonImpl(){};
    ~CSingletonImpl(){};
    CSingletonImpl(const CSingletonImpl& the);
    CSingletonImpl& operator=(const CSingletonImpl& other);
private:
    static std::mutex m_mutex;
    static std::atomic<CSingletonImpl*> m_taskQ;
};

std::mutex CSingletonImpl::m_mutex;
std::atomic<CSingletonImpl*> CSingletonImpl::m_taskQ;

2. Hungry man mode

1) Sample code

class CSingletonImpl
{
public:
    static CSingletonImpl* GetInstance()
    {
        return m_pInstance;
    }
private:
    CSingletonImpl(){};
    ~CSingletonImpl(){};
    CSingletonImpl(const CSingletonImpl& the);
    CSingletonImpl& operator=(const CSingletonImpl& other);
private:
    static CSingletonImpl* m_pInstance;
};

CSingletonImpl*CSingletonImpl::m_pInstance = new CSingletonImpl();

2) Description

This type of mode can be called Hungry Chinese style ------------------------------------------------------------------------------------------------------------------------------------------------------------------ No matter whether the GetInstance() member function is called or not, the singleton class object has been created as soon as the program is executed. In the implementation of Hungry-style singleton class code, it must be noted that if there are multiple .cpp source files in a project, and these source files contain initialization code for global variables, for example, the following code may exist in a .cpp:

int g_test = CSingletonImpl::GetInstance()->m_i; //m_i is an int type variable

Then such code is not safe, because the initialization order of global variables in multiple source files is uncertain, it is likely to cause the GetInstance() function to return nullptr, and accessing the m_i member variable at this time will definitely cause abnormal program execution. Therefore, the use of hungry Chinese-style singleton class objects should be executed after the program entry function, such as the main function.

Note: In the case of static variables initialized when the function is executed for the first time and basic type static variables initialized through compiler constants, do not refer to other singleton class objects in the destructor of the singleton class.

Guess you like

Origin blog.csdn.net/leiyang2014/article/details/132087371