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

在多线程环境下,传统的单例实现往往会出现竞争条件,导致多次实例化或数据破坏。C++11 引入的线程库和原子操作提供了多种安全的实现方式。下面先从经典的「Meyers 单例」谈起,再讨论 std::call_oncestd::atomic 以及 std::mutex 的组合方案,最后给出一个可扩展的、线程安全且延迟初始化的单例模板。


1. Meyers 单例(C++11 线程安全的实现)

class ThreadSafeSingleton {
public:
    static ThreadSafeSingleton& getInstance() {
        static ThreadSafeSingleton instance; // C++11 保证线程安全
        return instance;
    }

    // 删除拷贝构造和赋值
    ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
    ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;

    void doSomething() { /* ... */ }

private:
    ThreadSafeSingleton() { /* 可能耗时的初始化 */ }
    ~ThreadSafeSingleton() = default;
};

原理:C++11 规定 static 局部变量在第一次进入作用域时会被初始化,且此过程是原子化的。若多线程并发访问,编译器会在内部插入一个同步锁,确保只会有一次初始化。

优点

  • 简洁,几行代码即可实现。
  • 自动销毁(栈式析构)。

缺点

  • 延迟初始化(首次调用 getInstance 时才创建)。如果程序的启动阶段需要提前创建实例,可手动触发一次 getInstance()

2. std::call_once + std::once_flag

std::call_once 允许你在多线程环境中仅执行一次指定函数。适用于需要在构造函数外完成复杂初始化的场景。

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

    // 其他成员...
private:
    InitOnDemandSingleton() { /* 复杂初始化 */ }
    static std::unique_ptr <InitOnDemandSingleton> instance;
    static std::once_flag initFlag;
};

std::unique_ptr <InitOnDemandSingleton> InitOnDemandSingleton::instance;
std::once_flag InitOnDemandSingleton::initFlag;

优点

  • 初始化代码完全由你控制(如读取配置文件、网络请求等)。
  • 适用于在初始化时可能抛异常,需要捕获并重试的情况。

缺点

  • 代码略显冗长,需维护 once_flagunique_ptr

3. 采用原子指针 + 双检锁

如果你想在 C++11 之前的环境下实现线程安全(如 C++98/03),可以使用双检锁(Double-Check Locking)并配合 std::atomic 或者平台原子操作。

#include <atomic>
#include <mutex>

class AtomicSingleton {
public:
    static AtomicSingleton* getInstance() {
        AtomicSingleton* 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 AtomicSingleton();
                instance.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }

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

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

注意:双检锁在某些平台(如 ARM 的弱一致性)下仍存在潜在的问题。C++11 的 static 方式已解决大多数情形。


4. 可扩展的线程安全单例模板

为了在多个类中复用单例实现,下面给出一个简洁的模板。它使用 std::call_once 并且提供了 create() 供子类自定义实例化过程。

#include <memory>
#include <mutex>

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

    // 防止拷贝
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

protected:
    Singleton() = default;
    virtual ~Singleton() = default;

private:
    static std::unique_ptr <T> instance;
    static std::once_flag initFlag;
};

template <typename T>
std::unique_ptr <T> Singleton<T>::instance{nullptr};

template <typename T>
std::once_flag Singleton <T>::initFlag;

// 使用示例
class Logger : public Singleton <Logger> {
    friend class Singleton <Logger>;
private:
    Logger() { /* 打开日志文件 */ }
public:
    void log(const std::string& msg) { /* 写日志 */ }
};

// 访问方式
// Logger::getInstance().log("Hello");

优点

  • 只需一次声明,所有继承类都可获得线程安全单例。
  • 代码可读性高,维护简单。

5. 小结

  • Meyers 单例:最简洁、最安全,适合大多数场景。
  • std::call_once:适用于需要复杂初始化或异常处理的情况。
  • 双检锁 + 原子:兼容旧标准,但需谨慎使用。
  • 模板化单例:复用性强,适合大型项目。

在实际项目中,建议先从 Meyers 单例 开始,若需要自定义初始化过程再考虑 std::call_once 或模板方案。这样既能保持代码简洁,又能满足多线程安全的要求。

发表评论