多线程中的单例模式

单线程中的单例模式

在单线程中,实现一个单例模式是简单的:

cpp 复制代码
class Singleton
{
public:
    static Singleton* get_instance() 
    {
        if (instance_ == nullptr) 
        {
            instance_ = new Singleton();
        }
        return instance_;
    }

private:
    Singleton() = default;
    static Singleton* instance_;
};

// 在Singleton头文件中初始化静态成员变量
Singleton* Singleton::instance_ = nullptr;

在这个单例类中, 定义了静态(static)成员变量instance_,这意味着无论创建多少个类的对象,该静态成员都只有一个副本。由于instance_是私有成员,外部无法访问,需要在Singleton所在的头文件中进行初始化Singleton* Singleton::instance_ = nullptr;

在实例化对象时,通过Singleton::get_instance获取实例,根据if (instance_ == nullptr) 判断是否完成了实例化,如果没有,则new一个对象出来。在单线程中,这个过程无需担心数据的竞争问题。

多线程中的单例模式

但是在多线程中,可能出现这样的情况:

thread1和thread2同时执行Singleton::get_instance,这时if (instance_ == nullptr) 均成立,于是两个线程都会执行instance_ = new Singleton();,导致两次分配。两个线程操作的是不同的对象,导致行为上的不一致和不同步。

在多线程中,一种方式是使用双检查实现单例:

cpp 复制代码
class Singleton
{
public:
    static Singleton* get_instance() {
        Singleton* tmp = instance_.load(std::memory_order_acquire);
        if (tmp == nullptr) {
            std::unique_lock<std::mutex> lk(mutex_);
            tmp = instance_;
            if (tmp == nullptr) {
                tmp = new Singleton();
                instance_.store(std::memory_order_release);
            }
        }
        return tmp;
    }

private:
    Singleton() = default;
    static std::atomic<Singleton*> instance_;
    static std::mutex mutex_;
};

在这个实现方式中,将单例的成员变量instance_定义为原子变量,并增加了互斥量mutex_。在get_instance()中增加了带锁的双重检查。回过头来重新考虑之前的情况:

thread1和thread2同时执行Singleton::get_instance,这时if (instance_ == nullptr) 均成立,两者分别执行std::unique_lock<std::mutex> lk(mutex_);,假设thread2后执行,则由于先执行的thread1激活了锁,导致thread2被锁阻塞,thread1进入的则进行第二次检查,发现instance_ == nullptr成立,于是进行实例化,并在退出函数后自动释放锁。

随后thread2不再被锁阻塞,重新获取instance_的值,并进行第二次检查,此时instance_ == nullptr不再成立,于是跳过实例化,并返回最新的instance_

原子操作与乱序重排

除了锁的设置,这里还将instance_定义为原子变量,并通过load和store函数对其进行操作,这是为了防止编译优化过程中出现乱序重排。

程序在编译过程中,在不影响程序执行结果的前提下,通过重新编排代码的执行顺序,可以提高代码的执行效率,减少执行时间。

但重排后,在多线程的情况下,可能引发一些问题,比如下面这段代码:

cpp 复制代码
          int x = 0;     // global variable
          int y = 0;     // global variable
		  
Thread-1:              Thread-2:
x = 100;               while (y != 200)
y = 200;                   ;
                       std::cout << x;

正常情况下,在threa1中,执行完y=200;后,x的值应该为100,所以thread2的的输出结果为100。但由于threa1中的两条代码在顺序上没有依赖关系,所以在优化过程中,编译器可能将两者的顺序调换,得到如下的顺序:

cpp 复制代码
Thread-1:
y = 200;
x = 100;

在这种情况下,thread2的输出x的结果可能为原先的值,即0。

为防止出现编译优化引发的问题,在前面实现的多线程单例模式中,对instance_操作时分别采用了load()store()。这两者的除了读写instance_外,通过指定参数,还提供了重排的保证:instance_.load(std::memory_order_acquire)用于保证后面访存指令勿重排至此条指令之前instance_.store(std::memory_order_release)用于保证前面访存指令勿重排到此条指令之后。

参考:
atomic原子编程中的Memory Order
理解 C++ 的 Memory Order

相关推荐
重生之我在字节当程序员2 天前
如何实现单例模式?
单例模式
夕泠爱吃糖2 天前
如何实现单例模式?
单例模式
m0_607548762 天前
什么是单例模式
开发语言·javascript·单例模式
Am心若依旧4092 天前
[c++进阶(三)]单例模式及特殊类的设计
java·c++·单例模式
因特麦克斯2 天前
如何实现对象的克隆?如何实现单例模式?
c++·单例模式
狐拾叁4 天前
设计模式-创建者模式-单例模式(java版)
java·单例模式·设计模式
benben0444 天前
Unity3D仿星露谷物语开发5之角色单例模式
unity·单例模式·游戏引擎
Mr.135 天前
如何实现对象的克隆?如何实现单例模式?
c++·单例模式
missu2175 天前
C++中如何实现单例模式?
开发语言·c++·单例模式
行走的生活5 天前
为什么要用单例模式?
单例模式