双检锁问题

双检锁(Double-Checked Locking,DCL)在早期的 C++ 实现中存在问题,但在 C++11 及以后的标准中可以通过适当的处理来解决这些问题。下面详细分析双检锁存在的问题以及对应的解决办法。

早期双检锁存在的问题

指令重排序问题

在早期的 C++ 中,编译器和处理器为了提高性能,会对指令进行重排序。在双检锁实现单例模式时,创建对象的操作 instance = new Singleton(); 可以分解为以下三个步骤:

  1. 分配内存:为 Singleton 对象分配内存空间。
  2. 调用构造函数:在分配的内存空间上调用 Singleton 的构造函数来初始化对象。
  3. 将内存地址赋值给指针:将分配的内存地址赋值给 instance 指针。

然而,编译器和处理器可能会对这三个步骤进行重排序,比如将步骤 2 和步骤 3 的顺序交换,即先将内存地址赋值给 instance 指针,再调用构造函数。这样在多线程环境下,可能会出现以下情况:

  • 线程 A 进入 getInstance() 函数,发现 instancenullptr,于是加锁并开始创建对象。由于指令重排序,instance 指针先被赋值了内存地址,但对象还未完成初始化。
  • 此时线程 B 进入 getInstance() 函数,第一次检查发现 instance 不为 nullptr,就直接返回了 instance 指针。但实际上对象还未完成初始化,线程 B 使用这个未初始化的对象可能会导致程序崩溃或产生未定义行为。

以下是早期双检锁存在问题的代码示例:

#include 
#include 

class Singleton {
private:
    Singleton() {}
    static Singleton* instance;
    static std::mutex mtx;

public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            if (instance == nullptr) {
                instance = new Singleton();  // 可能发生指令重排序
            }
        }
        return instance;
    }
};

Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

C++11 及以后的解决办法

使用 std::atomic

在 C++11 及以后的标准中,引入了原子类型 std::atomic,可以保证原子操作,避免指令重排序问题。以下是使用 std::atomic 实现的双检锁单例模式:

#include 
#include 
#include 

class Singleton {
private:
    Singleton() {}
    static std::atomic<Singleton*> instance;
    static std::mutex mtx;

public:
    static Singleton* getInstance() {
        Singleton* tmp = instance.load(std::memory_order_relaxed);
        std::atomic_thread_fence(std::memory_order_acquire);
        if (tmp == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            tmp = instance.load(std::memory_order_relaxed);
            if (tmp == nullptr) {
                tmp = new Singleton();
                std::atomic_thread_fence(std::memory_order_release);
                instance.store(tmp, std::memory_order_relaxed);
            }
        }
        return tmp;
    }
};

std::atomic<Singleton*> Singleton::instance(nullptr);
std::mutex Singleton::mtx;

在上述代码中,使用 std::atomic 类型的 instance 来保证原子操作,同时使用 std::atomic_thread_fence 来插入内存屏障,防止指令重排序,确保对象在赋值给 instance 指针之前已经完成初始化。

使用 std::call_once

另一种更简洁的解决办法是使用 std::call_once,它可以保证某个函数在多线程环境下只被调用一次。以下是使用 std::call_once 实现的单例模式:

#include 
#include 

class Singleton {
private:
    Singleton() {}
    static Singleton* instance;
    static std::once_flag flag;

    static void init() {
        instance = new Singleton();
    }

public:
    static Singleton* getInstance() {
        std::call_once(flag, init);
        return instance;
    }
};

Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::flag;

在上述代码中,std::call_once 会保证 init 函数只被调用一次,从而确保单例对象只被创建一次,避免了双检锁的指令重排序问题。

综上所述,早期的双检锁存在指令重排序问题,但在 C++11 及以后的标准中,可以通过使用 std::atomicstd::call_once 来解决这些问题,实现线程安全的单例模式。

你可能感兴趣的:(【道阻且长C++】,c++)