C + + singleton mode and thread safety

C + + singleton mode and thread safety

The simplest singleton mode can be

// single thread safe version
class Singleton {
    public:
        static Singleton* GetInstance(int x = 0) {
            if (instance_ == NULL) {
                instance_ = new Singleton(x);
            }
            return instance_;
        }
        void Print() {
            std::cout << this->member_ << std::endl;
        }
    private:
        Singleton(int x = 3) : member_(x) {}
        int member_;
        static Singleton* instance_;  //declare a static member variable
};

Singleton* Singleton::instance_ = NULL;  //define a static member variable

However, in the case of multithreading, for example, thread A judges instance_ When it is empty, thread A suspends execution and hangs, while thread B judges instance_ If it is also empty, the constructor will be called to create the object and return. After thread A continues to resume running, the constructor will also be called to create the object and modify instance_ Pointer. This causes the first pointer to be modified, causing thread safety problems.

Based on this, you can judge instance_ To be unprecedented, lock first, and the thread that gets the lock can further judge the instance_ Is empty and further determines whether to create the object. The code is as follows

// thread safe, but inefficiency
class Singleton {
    public :
        static Singleton* GetInstance(int x = 0) {
            std::lock_guard<std::mutex> lock(mtx);
            if (instance_ == NULL) {
                instance_ = new Singleton(x);
            }
            return instance_;
        }
        void Print() {
            std::cout << this->member_ << std::endl;
        }

    private:
        Singleton(int x = 3) : member_(x) {}
        int member_;
        static Singleton* instance_;  //declare a static member variable    
};

Singleton* Singleton::instance_ = NULL;  //define a static member variable

However, this method has efficiency problems. Each time GetInstance() is called to obtain the object pointer, it needs to be locked. Obviously, we only want to lock the object when it is created for the first time, and we don't need to lock the subsequent access (the object has been created), so it's natural to think of judging instance again before locking_ Is it empty? The code is as follows:

// double-checked locking
class Singleton {
    public :
        static Singleton* GetInstance(int x = 0) {
            if (instance_ == NULL) {
                std::lock_guard<std::mutex> lock(mtx);
                if (instance_ == NULL) {
                    instance_ = new Singleton(x);
                    /*
                    instance_ =       //  point to memory
                        operator new(sizeof(Singleton));  // memory allocate
                    new (instance_) Singleton;  //  construct a object int the allocated memory, may occur exception
                    */
                }
            }
            return instance_b_;
        }
        void Print() { 
            std::cout << this->member_ << std::endl;
        }

    private:
        Singleton(int x = 3) : member_(x) {}
        int member_;
        static Singleton* instance_;  //declare a static member variable    
};

Singleton* Singleton::instance_ = NULL;  //define a static member variable

The above method solves the overhead caused by frequent locking, but there are still some problems. For new calls, the process is

  1. Allocate memory from heap
  2. Execute constructor in allocated memory
  3. Returns its pointer

In fact, the code execution order optimized by the compiler may not be like this, for example

instance_ = new Singleton(x);

May be equivalent to

instance_ = operator new(sizeof(Singleton));  // memory allocate
new (instance_) Singleton;  // placement new

Namely

  1. Allocate memory from heap
  2. Returns a pointer to the heap
  3. Executes a constructor on the memory pointed to by the pointer

In this case, if thread A is executing to instance_= operator new(sizeof(Singleton)); After that, the operation is suspended and suspended. Thread B enters GetInstance() and judges the instance_ If it is empty, it is found that it is not empty, so it returns instance_ Pointer, however, is uninitialized, so thread B's use of the pointer may cause undefined behavior. In order to prevent the execution order of the optimized code of the compiler from being inconsistent with our expectation (the order has been changed in steps 2 and 3 above), we want to ensure that instance is modified after new successfully calls the constructor_ Pointer, code as follows

// in-case exception occur when construct
class Singleton {
    public :
        static Singleton* GetInstance(int x = 0) {
            if (instance_ == NULL) {
                std::lock_guard<std::mutex> lock(mtx);
                if (instance_ == NULL) {
                    Singleton* temp = new Singleton(x);
                    instance_ = temp;
                }
            }
            return instance_;
        }
        void Print() { 
            std::cout << this->member_ << std::endl;
        }

    private:
        Singleton(int x = 3) : member_(x) {}
        int member_;
        static Singleton* instance_;  //declare a static member variable    
};

Singleton* Singleton::instance_ = NULL;  //define a static member variable

Here, we introduce the temp pointer to ensure that the instance is modified after the new call is successful_ Pointer. Replace new with the following

// in-case exception occur when construct
class Singleton {
    public :
        static Singleton* GetInstance(int x = 0) {
            if (instance_ == NULL) {
                std::lock_guard<std::mutex> lock(mtx);
                if (instance_ == NULL) {
                    Singleton* temp = operator new(sizeof(Singleton));  // memory allocate
										new (temp) Singleton;  // placement new
                    instance_ = temp;
                }
            }
            return instance_;
        }
        void Print() { 
            std::cout << this->member_ << std::endl;
        }

    private:
        Singleton(int x = 3) : member_(x) {}
        int member_;
        static Singleton* instance_;  //declare a static member variable    
};

Singleton* Singleton::instance_ = NULL;  //define a static member variable

However, for the above code, the compiler may still optimize it. The compiler will find that the temp variable only plays the role of passing value in the program and can be optimized. The optimized code will be instance_directly= new Singleton(x); , In order to prevent such optimization of the compiler, we can further use the volatile keyword to prevent the optimization of the compiler. The code is as follows

class Singleton {
    public :
        static Singleton* GetInstance(int x = 0) {
            if (instance_ == NULL) {
                std::lock_guard<std::mutex> lock(mtx);
                if (instance_ == NULL) {
                    Singleton* volatile temp = new Singleton(x);
                    instance_ = temp;
                }
            }
            return instance_;
        }
        void Print() { 
            std::cout << this->member_ << std::endl;
        }

    private:
        Singleton(int x = 3) : member_(x) {}
        int member_;
        static Singleton* instance_;  //declare a static member variable    
};

Singleton* Singleton::instance_ = NULL;  //define a static member variable

The above code declares the temp variable as volatile. Its purpose is to tell the compiler that the code blocks related to temp cannot be optimized, and the instruction sequence related to temp should be exactly the same as that seen by high-level languages.

However, there may still be a problem here. volatile only guarantees the operation order of the relevant codes of temp variables, but not the members of temp, such as the following codes

// in-case exception occur when construct
class Singleton {
    public :
        static Singleton* GetInstance(int x = 0) {
            if (instance_ == NULL) {
                std::lock_guard<std::mutex> lock(mtx);
                if (instance_ == NULL) {
                    Singleton* temp = operator new(sizeof(Singleton));  // memory allocate
										temp->member_ = x;  // construct
                    instance_ = temp;
                }
            }
            return instance_;
        }
        void Print() { 
            std::cout << this->member_ << std::endl;
        }

    private:
        Singleton(int x = 3) : member_(x) {}
        int member_;
        static Singleton* instance_;  //declare a static member variable    
};

Singleton* Singleton::instance_ = NULL;  //define a static member variable

When the constructor of temp assigns a value to the member variable of temp, temp - > member_= x. This operation and instance_= Temp may be reordered due to compiler optimization, if instance_= Temp before temp - > member_= X execution

instance_ = temp;
temp->member_ = x

When thread A finishes executing instance_= temp; Thread B obtains instance after it is suspended temporarily_ Then access its member variable member_ Undefined behavior will be triggered (if it is A pointer, A core will occur). Therefore, we need to ensure that the relevant operations of all member variables of class Singleton should also be volatile, so the code is as follows

class Singleton {
    public :
        static volatile Singleton* GetInstance(int x = 0) {
            if (instance_ == NULL) {
                std::lock_guard<std::mutex> lock(mtx);
                if (instance_ == NULL) {
                    volatile Singleton* temp = new volatile Singleton(x);
                    instance_ = temp;
                }
            }
            return instance_;
        }
        void Print() { 
            std::cout << this->member_ << std::endl;
        }

    private:
        Singleton(int x = 3) : member_(x) {}
        int member_;
        static volatile Singleton* instance_;  //declare a static member variable    
};

volatile Singleton* Singleton::instance_ = NULL;  //define a static member variable

Reference link: https://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf

Keywords: C++ Programming Design Pattern NLP number theory

Added by sheen.andola on Sat, 05 Mar 2022 11:48:04 +0200