c++ 如何优雅地写线程安全的单例

2019-10-14  本文已影响0人  lc_fan

提到线程安全, 大家都知道要加锁要原子化, 但是如何高效的实现呢?
在此介绍几种方法:
1. 直接加锁
2. Double Check Lock (DCL 双重校验加锁)
3. 静态局部变量初始化
4. std::call_once (c++11)

背景

下面看一个工作中实际遇到的case.
根据log发现某处加锁等待了8s多, 严重影响性能. (类名做保密处理)


lock.jpg

点进文件看到代码如下:

// 原始加锁, 等待8s
MySingleton* MySingleton::getInstance() {
    pthread_mutex_lock(&m_mutex);
    if(s_pInstance == NULL){
        s_pInstance = new MySingleton();
    }
    return s_pInstance;
}

改进一: Double Check Lock (DCL 双重校验加锁)

这时候绝大部分人都知道要改成 double check 的方式来避免绝大部分的加锁操作. 毕竟是c++面试几乎必考的知识点.

// 改进一: double check 加锁
MySingleton* MySingleton::getInstance() {
    if(s_pInstance == NULL){ // first check
        pthread_mutex_lock(&m_mutex);
        if(s_pInstance == NULL){ // second check
            s_pInstance = new MySingleton();
        }
    }
    return s_pInstance;
}

然而, 由于编译器会对具体执行步骤优化, 没法保证cpu完全按照我们高级语言顺序执行. 需要加 volatile, atomic, MemoryBarrier() 等操作.
另外也有罕见的, 单例尚未初始化完成, 指针就被赋值返回, 造成使用方崩溃.
总之DCL也有一定的风险和问题.

改进二: c++11 静态局部变量的初始化是线程安全的

c++11 引进了memory model,从此C++11也能识别线程这个概念了. 从而c++11 能够保证静态局部变量的初始化是线程安全的
因此可以改成下面的形式

// 改进二: c++11 静态局部变量的初始化是线程安全的
MySingleton* MySingleton::getInstance() {
    static MySingleton singleton();
    return &singleton;
}

改进三: std::call_once (c++11)

c++11 特性中方法 std::call_once(flag, callable, args) 可以通过不同的flag来保证callable这个函数不会被持有相同flag的来源同时调用执行.

// 改进三: std::call_once (c++11)
[std::once_flag](http://en.cppreference.com/w/cpp/thread/once_flag) g_flag;

MySingleton* MySingleton::getInstance() {
    if(s_pInstance == NULL){
        s_pInstance = std::call_once(g_flag, []()->MySingleton*{return new MySingleton();});
    }
    return s_pInstance;
}

总结

个人认为 std::call_once (c++11) 的方法最为优雅. 不过 静态局部变量初始化比较简洁. 二都ble check就是比较基础的方法.

另外有一篇大神的 博客, 推荐大家看看, 会对c++线程安全单例模式有更深刻的理解.

上一篇下一篇

猜你喜欢

热点阅读