如何在C++中实现线程安全的单例模式?

在 C++ 中实现线程安全的单例模式,最常用的方法是使用局部静态变量或 C++11 的原子操作与互斥锁。下面以几种典型实现方式为例,逐步说明如何保证单例在多线程环境下的安全性。

1. 局部静态变量(Meyers Singleton)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton inst;   // C++11 以后,局部静态变量初始化是线程安全的
        return inst;
    }
    // 禁止拷贝构造和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() {}
    ~Singleton() {}
};
  • 线程安全保证:自 C++11 起,编译器在第一次调用 instance() 时会对局部静态变量 inst 进行线程安全的初始化。若多线程同时进入该函数,只有一个线程会完成初始化,其他线程会等待。
  • 优点:实现简洁、延迟加载、无显式锁开销。
  • 缺点:在销毁阶段,如果有线程依旧在访问 instance(),可能导致访问已析构对象。

2. 带双重检查锁(Double-Checked Locking)

class Singleton {
public:
    static Singleton* getInstance() {
        if (!instance_) {                     // 第一次检查
            std::lock_guard<std::mutex> lock(mtx_);
            if (!instance_) {                 // 第二次检查
                instance_ = new Singleton();
            }
        }
        return instance_;
    }
    // ...
private:
    Singleton() {}
    static Singleton* instance_;
    static std::mutex mtx_;
};

Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mtx_;
  • 线程安全保证:双重检查锁通过先不加锁的检查减少锁竞争,再在锁内再次检查,保证了只有第一次调用会真正创建实例。由于 instance_ 是指针,且指针写入操作是原子的,C++11 的 std::atomicvolatile 可以进一步强化。
  • 优点:适用于 C++11 以前没有局部静态线程安全的实现。
  • 缺点:实现略显复杂,若未使用 std::atomic 或正确内存序,可能出现指令重排导致的可见性问题。

3. 使用 std::call_once

class Singleton {
public:
    static Singleton& getInstance() {
        std::call_once(initFlag_, []() { instance_ = new Singleton(); });
        return *instance_;
    }
    // ...
private:
    Singleton() {}
    static Singleton* instance_;
    static std::once_flag initFlag_;
};

Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
  • 线程安全保证std::call_once 确保传入的 lambda 只会被调用一次,内部使用的是线程安全的机制来管理一次性初始化。
  • 优点:实现简洁、性能良好(只需要一次锁),不受 new 的异常影响。
  • 缺点:需要手动维护指针,销毁时可能需要手动 delete,或者改为智能指针。

4. 使用 std::shared_ptrstd::once_flag

class Singleton {
public:
    static std::shared_ptr <Singleton> instance() {
        std::call_once(initFlag_, []() {
            instance_ = std::shared_ptr <Singleton>(new Singleton());
        });
        return instance_;
    }
    // ...
private:
    Singleton() {}
    static std::shared_ptr <Singleton> instance_;
    static std::once_flag initFlag_;
};

std::shared_ptr <Singleton> Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
  • 优点:使用 shared_ptr 自动管理生命周期,避免手动 delete。
  • 缺点:多次获取实例返回相同的 shared_ptr 对象,使用时需注意引用计数。

5. 关键点回顾

  1. 延迟加载:实例只在第一次访问时创建,节约资源。
  2. 线程安全:C++11 以后局部静态变量初始化已保证线程安全;否则需使用 std::mutexstd::call_once 等同步机制。
  3. 销毁顺序:若单例在程序结束前需要被销毁,最好使用 std::shared_ptrstd::unique_ptr,或在 atexit 注册销毁函数。
  4. 异常安全:若构造函数抛异常,应确保单例指针不被错误地设置。使用 std::call_once 或局部静态变量能自动处理。

6. 小结

在 C++ 中实现线程安全的单例模式最推荐的做法是使用局部静态变量(Meyers Singleton),因为它既简单又符合现代 C++ 标准。若需兼容旧标准或更细粒度的控制,可以采用 std::call_once 或双重检查锁实现。只要遵循上述原则,就能在多线程环境下安全、可靠地使用单例模式。

发表评论