多线程中的单例模式

2023-12-16 17:39:59

单线程中的单例模式

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

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();,导致两次分配。两个线程操作的是不同的对象,导致行为上的不一致和不同步。

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

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函数对其进行操作,这是为了防止编译优化过程中出现乱序重排。

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

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

          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中的两条代码在顺序上没有依赖关系,所以在优化过程中,编译器可能将两者的顺序调换,得到如下的顺序:

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

文章来源:https://blog.csdn.net/GodNotAMen/article/details/135033671
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。