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

在 C++17 之前,单例模式实现往往需要手动处理同步,容易出现错误。自 C++11 起,标准库提供了线程安全的静态局部变量初始化,简化了单例实现。下面给出几种常见实现方式,并讨论它们的优缺点。

1. 局部静态变量(最推荐)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;  // C++11 之后线程安全
        return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() { /* 业务初始化 */ }
};

优点

  • 代码简洁,几行即可完成。
  • 依赖标准库实现,线程安全性保证。
  • 对象延迟加载,只有真正访问 instance() 时才创建。

缺点

  • 只能在函数内部使用局部静态,若想让单例在程序结束前被显式销毁,需自己手动实现。
  • 如果单例构造时抛异常,后续访问会再次尝试构造。

2. std::call_once + std::unique_ptr

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag, []{
            ptr.reset(new Singleton);
        });
        return *ptr;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() { /* 初始化 */ }
    static std::once_flag initFlag;
    static std::unique_ptr <Singleton> ptr;
};

std::once_flag Singleton::initFlag;
std::unique_ptr <Singleton> Singleton::ptr;

优点

  • 能在程序结束前主动销毁单例。
  • 兼容 C++11 之前的编译器(使用 std::call_once 也需要 C++11,但构造器不需要 C++11 静态初始化)。

缺点

  • 代码量略多,维护成本增加。
  • std::unique_ptr 的析构顺序可能与其他全局对象产生竞争。

3. 双重检查锁(不推荐)

class Singleton {
public:
    static Singleton* instance() {
        if (!ptr) {
            std::lock_guard<std::mutex> lock(mtx);
            if (!ptr) ptr = new Singleton();
        }
        return ptr;
    }
private:
    Singleton() {}
    static Singleton* ptr;
    static std::mutex mtx;
};
Singleton* Singleton::ptr = nullptr;
std::mutex Singleton::mtx;

缺点

  • 需要对 ptr 进行原子读写,存在可见性问题。
  • 若编译器不采用合适的内存屏障,可能导致多线程下实例不完整。
  • 代码更易出错,建议使用前两种方式。

4. Meyers 单例(静态局部变量)与全局对象

如果单例本身不需要动态销毁,或者不需要在 main 之前使用,Meyers 单例是最简单且最安全的方式。它利用 C++11 对局部静态变量初始化的线程安全保证,避免了手动加锁。

如何在单例中使用依赖注入

在复杂项目中,单例往往需要依赖其他组件。可以将依赖以构造参数形式传入,并通过 std::function 或者 std::unique_ptr 存储。

class Logger {
public:
    void log(const std::string& msg) { /* ... */ }
};

class Config {
public:
    std::string get(const std::string& key) const { /* ... */ }
};

class Singleton {
public:
    static Singleton& instance(Logger* logger = nullptr, Config* cfg = nullptr) {
        static Singleton s;
        if (logger) s.setLogger(logger);
        if (cfg)    s.setConfig(cfg);
        return s;
    }
    // 业务接口...
private:
    Singleton() = default;
    void setLogger(Logger* l) { logger = l; }
    void setConfig(Config* c) { config = c; }
    Logger* logger = nullptr;
    Config* config = nullptr;
};

这样可以在第一次访问时注入依赖,后续访问不受影响。

小结

  • 推荐:使用 C++11 的局部静态变量实现(Meyers 单例)。
  • 需要显式销毁:使用 std::call_once + std::unique_ptr
  • 避免:手写双重检查锁,除非有特殊需求且你非常了解内存模型。
  • 依赖注入:可通过可选参数或单独的 set 方法实现。

通过上述方案,你可以在 C++ 项目中安全、可靠地实现单例模式,既兼顾了性能,又遵循了现代 C++ 的最佳实践。

发表评论