Talking about the thread safety of singleton mode

1. Deeply understand the initialization of static local variables in the function
1.1. Scene analysis
First analyze a piece of code:

#include<iostream>
using namespace std;
void foo(bool recur);
int bar(bool recur) {
    
    
 cout<<"bar called\n";
 if(recur) {
    
    
    foo(false);
 }
 return 0xFAFAFA;
}
void foo(bool recur) {
    
    
 cout<<"foo called\n";
 static int i = bar(recur);
 cout<<"Static is:"<< i<<"\n";
}
int main() {
    
    
 foo(true);
 return 0;
}

The above code looks normal, but after running:

$ g++ test.cpp
$ ./a.out
foo called
bar called
foo called
terminate called after throwing an instance of '__gnu_cxx::recursive_init_error'
  what():  std::exception
Aborted (core dumped)

1.2. Introduction
First introduce recursive locks. Recursive locks refer to a lock in a single thread. For example, the same function uses lock multiple times. Unlock nesting will not lock, but multi-threaded access will cause blocking. Non-recursive locks do not matter. Single-threaded or multi-threaded will block.
When we initialize a local static variable, what will happen inside it?

  if (obj_guard.first_byte == 0)
  {
    
    
    if ( __cxa_guard_acquire (&obj_guard) ) {
    
    
      try {
    
    
      // ... initialize the object ...;
      } catch (...) {
    
    
        __cxa_guard_abort (&obj_guard);
        throw;
      }
      // ... queue object destructor with __cxa_atexit() ...;
      __cxa_guard_release (&obj_guard);
    }
  }
 guard_for_bar 是⼀个⽤来保证线程安全和⼀次性初始化的整型变量,是编译器⽣成的,存储在 bss段。它的最低的⼀个字节被⽤作相应静态变量是否已被初始化的标志, 若为 0 表示还未被初始化,否则表示已被初始化,它的第二个标志是判断是否正在使用的。__cxa_guard_acquire 实际上是⼀个加锁的过程, 相应的 __cxa_guard_abort 和__cxa_guard_release 释放锁 。

We can imitate it with c++ code

// Double check that the initializer has not already been run
if ( initializerHasRun(guard_object) ) // 如果对象已被初始化
    return 0;
// We now need to acquire a lock that allows only one thread
// to run the initializer. If a different thread calls
// __cxa_guard_acquire() with the same guard object, we want
// that thread to block until this thread is done running the
// initializer and calls __cxa_guard_release(). But if the same
// thread calls __cxa_guard_acquire() with the same guard object,
// we want to abort.
// To implement this we have one global pthread recursive mutex
// shared by all guard objects, but only one at a time.
int result = ::pthread_mutex_lock(guard_mutex());
if ( result != 0 ) {
    
    
	abort_message("__cxa_guard_acquire(): pthread_mutex_lock failed with %d\n", result);
} 
// At this point all other threads will block in __cxa_guard_acquire()
// Check if another thread has completed initializer run
if ( initializerHasRun(guard_object) ) {
    
     // 再次判断, 对象是否已被其他线程初始化
    int result = ::pthread_mutex_unlock(guard_mutex());
    if ( result != 0 ) {
    
    
    abort_message("__cxa_guard_acquire(): pthread_mutex_unlock failed with %d\n",         result);

} 

    return 0;

} 
// The pthread mutex is recursive to allow other lazy initialized
// function locals to be evaluated during evaluation of this one.
// But if the same thread can call __cxa_guard_acquire() on the
// *same* guard object again, we call abort();
if ( inUse(guard_object) ) {
    
    
	abort_message("__cxa_guard_acquire(): initializer for function local static variable called enclosing function\n");
} 
// mark this guard object as being in use
setInUse(guard_object);
// return non-zero to tell caller to run initializer
return 1;
}

}
The reason for the program dump above is that it was not initialized at the beginning and then got the lock and then called bar() (not unlocked at this time) to solve the problem and then call foo. It must be uninitialized at this time, and then there is no problem with locking. It is found that it is in use state, that is, in use state, and an exception is thrown at this time. Reflected in this:

if ( inUse(guard_object) ) {
    
    
	abort_message("__cxa_guard_acquire(): initializer for function local static variable called enclosing function\n");
}
如果局部静态变量内部不是用的递归锁而是用的非递归锁会出现什么问题呢,那应该就是死锁了。
这里有个说的很好的引入一下https://manishearth.github.io/blog/2015/06/26/adventures-in-systems-programming-c-plus-plus-local-statics/

Second, the singleton mode
2.1, the original lazy singleton mode The lazy singleton is to create the singleton object only when the singleton object needs to be used

class Singleton {
    
    
private:
    static Singleton *m_singleton;
    Singleton() = default;  // 自动生成默认构造函数
    Singleton(const Singleton& s) = delete;    // 禁用拷贝构造函数
    Singleton& operator=(const Singleton& s) = delete; // 禁用拷贝赋值操作符
    class GarbageCollector {
    
    
    public:
        ~GarbageCollector() {
    
    
            cout << "~GarbageCollector\n";
            if (Singleton::m_singleton) {
    
    
                cout << "free m_singleton\n";
                delete Singleton::m_singleton;
                Singleton::m_singleton = nullptr;
            }
        }
    };
    static GarbageCollector m_gc;
public:
    static Singleton* getInstance(){
    
    
        if (Singleton::m_singleton == nullptr){
    
    
            std::this_thread::sleep_for(std::chrono::milliseconds(10)); //休眠,模拟创建实例的时间
            m_singleton = new Singleton();
        }
        return m_singleton;
    }
};
// 必须在类外初始化
Singleton* Singleton::m_singleton = nullptr;
Singleton::GarbageCollector Singleton::m_gc;

2.2, thread-safe lazy singleton mode

// 2 线程安全的懒汉式单例模式
//线程安全的懒汉式单例
class Singleton {
    
    
private:
    static Singleton *m_singleton;
    static mutex m_mutex;
    Singleton() = default;
    Singleton(const Singleton& s) = delete; // 禁用拷贝构造函数
    Singleton& operator=(const Singleton& s) = delete;  // 禁用拷贝赋值操作符
    class GarbageCollector {
    
    
    public:
        ~GarbageCollector() {
    
    
            cout << "~GarbageCollector\n";
            if (Singleton::m_singleton) {
    
    
                cout << "free m_singleton\n";
                delete Singleton::m_singleton;
                Singleton::m_singleton = nullptr;
            }
        }
    };
    static GarbageCollector m_gc;
public:
    static Singleton* getInstance() {
    
     // 加锁的粒度大,效率较低, 对高并发的访问
        m_mutex.lock(); // 加锁,保证只有一个线程在访问下面的语句
        if (Singleton::m_singleton == nullptr){
    
    
//            std::this_thread::sleep_for(std::chrono::milliseconds(1000)); //休眠,模拟创建实例的时间
            m_singleton = new Singleton();
        }
        m_mutex.unlock();//解锁
        return m_singleton;
    }
};
Singleton* Singleton::m_singleton = nullptr;
mutex Singleton::m_mutex;
Singleton::GarbageCollector Singleton::m_gc;
这种方式的确定很明显,在高并发访问实例时性能低下

2.3 Check whether the instance is created again after locking the initial instance statement

class Singleton {
    
    
private:
    static Singleton *m_singleton;
    static mutex m_mutex;
    Singleton() = default;
    Singleton(const Singleton& s) = default;
    Singleton& operator=(const Singleton& s) = default;
    class GarbageCollector {
    
    
    public:
        ~GarbageCollector() {
    
    
            cout << "~GarbageCollector\n";
            if (Singleton::m_singleton) {
    
    
                cout << "free m_singleton\n";
                delete Singleton::m_singleton;
                Singleton::m_singleton = nullptr;
            }
        }
    };
    static GarbageCollector m_gc;
public:
    void *getSingletonAddress() {
    
    
        return m_singleton;
    }
    static Singleton* getInstance() {
    
    
        if (Singleton::m_singleton == nullptr){
    
    
            m_mutex.lock();  // 加锁,保证只有一个线程在访问线程内的代码
            if (Singleton::m_singleton == nullptr) {
    
     //再次检查
                m_singleton = new Singleton();  // 对象的new不是原子操作 1、分配内存,2 调用构造,3 赋值操作,到第3步的时候才是m_singleton非空
                                                //  1、分配内存,2 赋值操作 3 调用构造,到第2步的时候才是m_singleton非空
            }
            m_mutex.unlock();//解锁
        }
        return m_singleton;
    }
};
Singleton* Singleton::m_singleton = nullptr;
mutex Singleton::m_mutex;
Singleton::GarbageCollector Singleton::m_gc

There seems to be no problem in this way, but there are still problems in practice.
Double check lock, but because the memory read and write reorder is not safe because C++ creates an object, it will perform three operations: 1, allocate memory, 2 call construction, and 3 assignment operation.
However, modern CPUs and compilers may be out of order under high concurrency. The operations are rearranged, so the second step of creating the object new CSingleton may be called later than the third step,
resulting in undefined behavior.
Because m_singleton = new Singleton(); is not an atomic operation, there may be the following situations:
1. Allocate memory, 2 call construction, 3 assignment operation, and m_singleton is not empty until the third step.
1. Allocate memory, 2 assignment operation, 3 call structure, and m_singleton is not empty until the second step.
The first is ideal, but there will be problems in the second. For example, when 1 allocates memory and 2 assigns a value, there is a third step. If there is another thread accessing the getInstance() function and judge Singleton::m_singleton is non-NULL, and then returns to other operations that called Singleton externally (assuming that the third step of the call construction has not been completed at this time). At this time, it will cause the behavior of the program to dump.
2.4, Cross-platform implementation after C++ 11 version

class Singleton {
    
    
private:
    static std::atomic<Singleton*> m_instance;
    static std::mutex m_mutex;
    Singleton() = default;
    Singleton(const Singleton& s) = default;
    Singleton& operator=(const Singleton& s) = default;
    class GarbageCollector {
    
    
    public:
        ~GarbageCollector() {
    
    
            cout << "~GarbageCollector\n";
            Singleton* tmp = m_instance.load(std::memory_order_relaxed);
            if (tmp) {
    
    
                cout << "free m_singleton: " << tmp << endl;
                delete tmp;
            }
        }
    };
    static GarbageCollector m_gc;
public:
    void *getSingletonAddress() {
    
    
        return m_instance;
    }
    static Singleton* getInstance() {
    
    
        Singleton* tmp = m_instance.load(std::memory_order_relaxed);

        std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence

        if (tmp == nullptr) {
    
    
            std::lock_guard<std::mutex> lock(m_mutex);
            tmp = m_instance.load(std::memory_order_relaxed);
            if (tmp == nullptr) {
    
    
                tmp = new Singleton();  // 1、分配内存,2 调用构造,3 赋值操作
                std::atomic_thread_fence(std::memory_order_release);//释放内存fence
                m_instance.store(tmp, std::memory_order_relaxed);
            }
        }
        return tmp;
    }
};
std::atomic<Singleton*>  Singleton::m_instance;
std::mutex Singleton::m_mutex;
Singleton::GarbageCollector Singleton::m_gc;

2.5 Lazy man is recommended

// 懒汉式
class Singleton {
    
    
private:
//    Singleton() = default;  // 自动生成默认构造函数
    Singleton() {
    
                               // 构造函数 会影响局部静态变量, 不能用隐式的构造函数
        cout << "Singleton construct\n";
    }
    Singleton(const Singleton& s) = delete;    // 禁用拷贝构造函数
    Singleton& operator=(const Singleton& s) = delete; // 禁用拷贝赋值操作符
public:
    static Singleton* getInstance(){
    
    
        static Singleton s_singleton;  // C++11线程安全, C++11之前不是线程安全  __cxa_guard_acquire 和 __cxa_guard_release
        return &s_singleton;
    }
};

This method is simple and thread-safe. The local static variable static Singleton s_singleton we discussed above is thread-safe for internal locking. Therefore, this method is recommended in actual development.
2.6 Hungry Chinese style, initialize before the main function runs, absolutely safe

// 饿汉式,在main函数运行前初始化,绝对安全
class Singleton {
    
    
private:
    //    Singleton() = default;   // 自动生成默认构造函数
    Singleton() {
    
    
        cout << "Singleton construct\n";
    }
    Singleton(const Singleton& s) = delete;    // 禁用拷贝构造函数
    Singleton& operator=(const Singleton& s) = delete; // 禁用拷贝赋值操作符
    static Singleton m_singleton;
public:
    static Singleton* getInstance(){
    
    
        return &m_singleton;
    }
};
Singleton Singleton::m_singleton;

3. Summary
There are many versions of singleton mode. The fifth type of lazy man is recommended here. It is roughly divided into the lazy man type. Generally speaking, the definition generated before the start of the main function belongs to the hungry man type. The call generated after the main function runs belongs to the lazy man type. The lazy man mode is initialized only when it is called for the first time. , The hungry man mode program is initialized at the beginning, and the space is exchanged for time.

Guess you like

Origin blog.csdn.net/wangrenhaioylj/article/details/108937427