专栏首页C++的沉思C++多线程如何获取真正安全的单例
原创

C++多线程如何获取真正安全的单例

编译器顺序问题

举一个例子,假如有两个全局变量:

int x = 0;
int y = 0;

然后我们在一个线程里执行:

x = 1;
y = 2;

在另一个线程里执行:

if (y == 2) {
    x = 3;
    y = 4;
}

如果你认为有两种可能,1、2和3、4的话,那说明你是按典型的程序员思维看问题的--没有像编译器和处理器一样处理问题。事实上, 1、4也是一种可能的结果。有两个基本原因造成这一后果:

  • 编译器没有义务一定按代码给出的顺序产生代码。事实上会根据上下文调整代码的执行顺序,使其最有利于处理器的架构,是优化中很重要的一步。
  • 在多处理器架构中,各个处理器可能产生缓存不一致问题。取决于具体的处理器类型、缓存策略和变量地址,对变量 y 的写入有可能先反应到主存中去。

双重检查锁定

在多线程对单例进行初始化的过程中,有一个双重检查锁定的技巧,基本实现如下:

class singleton {
public:
    static singleton* instance() 
    {
        if (inst_ptr_ == nullptr)
        {
            std::lock_guard<std::mutex> lk(mutex_);
            if (inst_ptr_ == nullptr) {
                inst_ptr_ = new singleton();
            }
        }
        return inst_ptr_;
    }
private:
    singleton() {}
    singleton(const singleton&) {}
    singleton& operator = (const singleton&);
private:
    static singleton* inst_ptr_;
    static std::mutex mutex_;
};

singleton* singleton::inst_ptr_ = nullptr;
std::mutex singleton::mutex_;

代码目的是消除大部分执行路径上的加锁开销。意图是:如果 inst_ptr_ 没有被初始化,执行才会进入加锁的路径,防止单例被构造多次;如果 inst_ptr_ 已经被初始化,那它就会被直接返回,不会产生额外开销。这看起来很棒,但直到2000年才有人发现了漏洞,而且在每个语言都发现了,原因是内存读写是乱序的。即创建实例 inst_ptr_ = new singleton(); 是其实分如下三个步骤完成:

  1. 分配 singleton 对象所需的内存空间;
  2. 在分配的内存处构造 singleton 对象;
  3. 将内存的地址赋给指针 inst_ptr_。

上面这三个步骤如果是按顺序进行的,那上面的双重检查锁定的就没有任何问题。但除了确定步骤1首先执行,2和3的顺序是不确定的。假如线程A按1、3、2的顺序执行,当执行完3后,就切到线程B,因为 inst_ptr_ 不为 nullptr 直接 return inst_ptr_ 得到一个对象,而这个对象没有被构造!严重 bug 就出现了!

C++11跨平台实现

在C++11中可以用原子操作实现真正线程安全的单例模式,具体实现如下:

class singleton {
public:
    static singleton* instance() 
    {
        singleton* ptr = inst_ptr_.load(std::memory_order_acquire);
        if (inst_ptr_ == nullptr)
        {
            std::lock_guard<std::mutex> lk(mutex_);
            ptr = inst_ptr_.load(std::memory_order_relaxed);
            if (inst_ptr_ == nullptr) {
                ptr = new singleton();
                inst_ptr_.store(ptr, std::memory_order_release);
            }
        }
        return ptr;
    }
private:
    singleton() {}
    singleton(const singleton&) {}
    singleton& operator = (const singleton&);
private:
    static std::atomic<singleton*> inst_ptr_;
    static std::mutex mutex_;
};

std::atomic<singleton*> singleton::inst_ptr_;
std::mutex singleton::mutex_;

Scott Meyers 优雅的单例模式

class singleton {
public:
    static singleton& instance() 
    {
        static singleton instance_;
        return instance_;
    }
private:
    singleton() {}
    singleton(const singleton&) {}
    singleton& operator = (const singleton&);
};

Scott Meyers 在《Effective C++》中的提出另一种更优雅的单例模式实现,使用 local static 对象(函数内的 static 对象)。当第一次访问 instance() 方法时才创建实例。C++0x之后该实现是线程安全的,C++0x之前仍需加锁。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • LRU缓存淘汰机制C++实现

    LRU 是 Least Recently Used 的简写,字面意思是最近最少使用。

    evenleo
  • 外观模式-分析和C++实现

    由于上面外观模式的结构过于抽象,因此把它具体点。假设系统内有三个模块,分别是AModule,BModule和CModule,它们分别有一个示意的方法,那么整体结...

    evenleo
  • 归并快排算法比较及求第K大元素

    核心思想:将数组从中间分成前后两部分,然后对前后两部分分别进行排序,再将排序好的两个部分有序合并在一起,这样整个数组有序。全文图示来源于王争的《数据结构和算法之...

    evenleo
  • 新英格兰医学:EEG机器学习:急性脑损伤临床无反应患者脑激活的检测

    请点击上面“思影科技”四个字,选择关注我们,思影科技专注于脑影像数据处理,涵盖(fMRI,结构像,DTI,ASL,EEG/ERP,FNIRS,眼动)等,希望专业...

    用户1279583
  • MAC版画图软件 paintbrush 推荐,类似 windows 上系统自带的画图软件

    不想开photoshop这么重的软件,但是对于屏幕截图有需要有一点处理。这时候我想起 windows上画图的好了。 搜索了一下,知道了 paintbrush 这...

    FungLeo
  • 云原生分布式深度学习初探

    大规模数据以及大型的神经网络结合在很多机器学习的任务上带来了超凡的表现。在训练深度学习模型的时候,当数据以及参数量变大的时候计算资源是决定我们算法迭代速度的关键...

    溪歪歪
  • 入门Github,这篇文章够了!

    至于Github的介绍我觉得没有什么比百度更全面的了,都要入门了总该知道这是干啥的吧。

    格姗知识圈
  • 原 荐 SpringCloud2.0 Eur

    kinbug [进阶者]
  • SpringCloud2.0 Eureka集群 高可用的认证服务实现与搭建

    SpringCloud-2.0.2.RELEASE Eureka认证后,服务注册失败问题。

    吴生
  • Flutter自制插件之r_calendar日历插件

    ??Flutter日历插件,支持自定义日历,月视图/周视图切换、点击拦截、单选(切换月自动选)、多选(散选/聚选) .-------------------...

    rhyme_lph

扫码关注云+社区

领取腾讯云代金券