前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++多线程如何获取真正安全的单例

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

原创
作者头像
evenleo
修改2020-04-13 11:24:00
2.4K0
修改2020-04-13 11:24:00
举报
文章被收录于专栏:C++的沉思

编译器顺序问题

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

代码语言:javascript
复制
int x = 0;
int y = 0;

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

代码语言:javascript
复制
x = 1;
y = 2;

在另一个线程里执行:

代码语言:javascript
复制
if (y == 2) {
    x = 3;
    y = 4;
}

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

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

双重检查锁定

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

代码语言:c++
复制
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中可以用原子操作实现真正线程安全的单例模式,具体实现如下:

代码语言:c++
复制
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 优雅的单例模式

代码语言:c++
复制
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之前仍需加锁。

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 编译器顺序问题
  • 双重检查锁定
  • C++11跨平台实现
  • Scott Meyers 优雅的单例模式
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档