一、深⼊理解函数内静态局部变量初始化
1.1、场景分析
首先分析一段代码:
#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;
}
上面的代码看起来很正常,但是运行后:
$ 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、引入
首先引入递归锁,递归锁是指一把锁在单线程中比如同一个函数多次使用lock,unlock嵌套不会锁住,但是多线程访问会引起阻塞,非递归锁的话是不管单线程还是多线程都会阻塞。
当我们对一个局部静态变量初始化时,看看它内部会做什么操作呢?
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 释放锁 。
我们可以用c++代码模仿
// 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;
}
}
上面引起程序dump的原因是,在刚开始时候未初始化然后取得锁 后来调用bar()(此时并没有解锁)解决再调用foo此时肯定是未初始化状态,然后锁住没有问题,再后面发现是in use状态也就是在使用状态,此时抛出异常。体现在这:
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/
二、单例模式
2.1 、原始懒汉式单例模式 懒汉式单例就是需要使用这个单例对象的时候才去创建这个单例对象
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、线程安全的懒汉式单例模式
// 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锁住初始化实例语句之后再次检查实例是否被创建
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
这种方式看起来没有任何问题,但实际还是有问题的。
双检查锁,但由于内存读写reorder不安全 因为C++创建对象时,会执行1、分配内存,2 调用构造,3 赋值操作三步操作,
然而现代CPU和编译器高并发下可能会进行乱序重排操作,因而创建对象new CSingleton的第2步可能会晚于第3步进行指令调用,
因而导致出现未定义的的行为。
因为 m_singleton = new Singleton(); 不是原子操作,所以可能有如下情况:
1.分配内存,2 调用构造,3 赋值操作,到第3步的时候才是m_singleton非空。
1.分配内存,2 赋值操作 3 调用构造,到第2步的时候才是m_singleton非空。
第一种是理想的没问题,但是第二中会出现问题,比如当1 分配内存,2 赋值操作 此时还有第三步,假如此时刚好有另外一个线程线程访问getInstance()函数 并且判断Singleton::m_singleton为非NULL,然后返回了在外部调用Singleton的其他操作(假设此时第三步调用构造还没完成)这时将会引起程序dump的行为。
2.4、C++ 11版本之后的跨平台实现
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 推荐懒汉式
// 懒汉式
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;
}
};
这种方式简洁,并且线程安全,局部静态变量static Singleton s_singleton 我们上面讨论过内部会加锁是线程安全的。所以在实际开发中推荐用这种方式。
2.6 饿汉式,在main函数运行前初始化,绝对安全
// 饿汉式,在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;
三、总结
单例模式版本很多,这里推荐用第五种懒汉式。大体分为懒汉式饿汉式,总的来说在main函数开始之前就生成定义的属于饿汉式,在main函数运行后调用生成的属于懒汉式,懒汉模式第一次调用的时候才才初始化,饿汉模式程序开始的时候就初始化,以空间换时间。