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

在多线程环境下,单例模式的实现需要保证同一时刻只有一个线程可以创建实例。最常用的方法是使用 C++11 的静态局部变量,它在首次调用时会以线程安全的方式进行初始化。另外,也可以使用双重检查锁定(Double-Check Locking)配合 std::atomic 或者 std::call_once 来实现。下面分别给出两种实现方式,并对其优缺点进行分析。


1. 基于 C++11 静态局部变量的实现

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;   // C++11 规定在第一次调用时线程安全初始化
        return instance;
    }

    // 禁止拷贝和移动构造
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;

private:
    Singleton() { /* 可能的资源初始化 */ }
    ~Singleton() { /* 资源释放 */ }
};

优点

  • 代码简洁,编译器负责实现线程安全。
  • 初始化一次后,后续访问几乎没有开销。

缺点

  • 如果 Singleton 的构造函数抛异常,程序将无法恢复。
  • 不能在程序结束前显式销毁实例(除非使用 std::unique_ptr 结合 std::shared_ptr 的自定义删除器)。

2. 基于 std::call_once 的实现

#include <mutex>

class Singleton {
public:
    static Singleton& getInstance() {
        std::call_once(initFlag, [](){ instance.reset(new Singleton()); });
        return *instance;
    }

    // 禁止拷贝和移动
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;

private:
    Singleton() {}
    ~Singleton() {}

    static std::unique_ptr <Singleton> instance;
    static std::once_flag initFlag;
};

std::unique_ptr <Singleton> Singleton::instance = nullptr;
std::once_flag Singleton::initFlag;

优点

  • std::call_once 可以保证初始化代码只执行一次,且异常安全。
  • 可以在需要时显式销毁实例(例如 instance.reset())。

缺点

  • 需要手动维护指针和销毁逻辑,代码稍显复杂。

3. 双重检查锁定(Double-Check Locking)

虽然在 C++11 之前的实现常见,但在现代 C++ 中不再推荐,因为 std::atomic 不能保证对对象完整性的可见性。示例代码仅供参考。

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

    // 禁止拷贝和移动
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mtx;

优点

  • 仅在首次创建实例时产生锁开销。

缺点

  • 需要手动保证内存可见性,容易出错。
  • 现代 C++ 提供了更安全、更简洁的方案。

小结

在 C++11 及以后,推荐使用 静态局部变量std::call_once 来实现线程安全的单例。前者最简洁,后者在异常安全和显式销毁方面更有优势。双重检查锁定已被视为过时实践,除非有极端性能要求且已在细节上做足够保证,否则不建议使用。

通过上述三种实现方式,你可以根据项目需求、异常处理策略和资源释放时机,选择最合适的单例实现方式。

发表评论