多线程中的单例模式

单线程中的单例模式

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

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

相关推荐
zh_xuan4 小时前
c++ 单例模式
开发语言·c++·单例模式
西北大程序猿15 小时前
单例模式与锁(死锁)
linux·开发语言·c++·单例模式
找不到、了3 天前
实现单例模式的常见方式
java·开发语言·单例模式
不愧是你呀5 天前
C++中单例模式详解
网络·c++·windows·单例模式
变身缎带5 天前
Unity中的MonoSingleton<T>与Singleton<T>
unity·单例模式·c#·游戏引擎
小吴同学·7 天前
OPC Client第6讲(wxwidgets):Logger.h日志记录文件(单例模式);登录后的主界面
开发语言·c++·单例模式·wxwidgets
勤奋的知更鸟8 天前
Java 单例模式详解
java·开发语言·单例模式
ailinghao8 天前
单例模式的类和静态方法的类的区别和使用场景
flutter·单例模式
XiaoLeisj8 天前
【JUC】深入解析 JUC 并发编程:单例模式、懒汉模式、饿汉模式、及懒汉模式线程安全问题解析和使用 volatile 解决内存可见性问题与指令重排序问题
javascript·安全·单例模式
charlie1145141919 天前
从C++编程入手设计模式1——单例模式
c++·单例模式·设计模式·架构·线程安全