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

在多线程环境中,单例模式常用于共享资源,例如日志系统或数据库连接池。实现线程安全的单例有几种常见做法,下面详细介绍两种最常用且简洁的实现方式,并比较它们的优缺点。


1. C++11 std::call_once + std::once_flag

C++11 引入了 std::call_oncestd::once_flag,可保证某个函数仅被调用一次,即使在多线程竞争时也不需要手动加锁。

#include <mutex>
#include <memory>

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

    void log(const std::string& msg) {
        std::lock_guard<std::mutex> lock(mtx);
        // 简单示例:直接输出
        std::cout << msg << std::endl;
    }

private:
    Logger() = default;
    ~Logger() = default;
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

    static std::once_flag initFlag;
    static std::unique_ptr <Logger> instancePtr;
    std::mutex mtx;
};

std::once_flag Logger::initFlag;
std::unique_ptr <Logger> Logger::instancePtr = nullptr;

优点

  • 简洁:不需要显式锁,减少代码量。
  • 性能std::call_once 内部实现为原子操作,开销低。
  • 线程安全:在任何线程中调用 instance() 都是安全的。

缺点

  • 无法自定义销毁顺序:对象会在程序退出时被自动销毁,若有依赖关系需手动管理。

2. 局部静态变量(Meyer’s Singleton)

C++11 起,局部静态变量的初始化是线程安全的。只需将实例定义为局部静态即可。

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

    // 读取配置
    std::string get(const std::string& key) const {
        std::lock_guard<std::mutex> lock(mtx);
        auto it = data.find(key);
        return it != data.end() ? it->second : "";
    }

    void set(const std::string& key, const std::string& value) {
        std::lock_guard<std::mutex> lock(mtx);
        data[key] = value;
    }

private:
    Config() = default;
    ~Config() = default;
    Config(const Config&) = delete;
    Config& operator=(const Config&) = delete;

    std::unordered_map<std::string, std::string> data;
    mutable std::mutex mtx;
};

优点

  • 代码最简:不需要额外的 once_flag 或手动锁。
  • 天然延迟初始化:首次调用时才会构造,避免不必要的开销。

缺点

  • 无法显式销毁:如果对象的析构顺序重要,需要特殊处理(例如使用 std::shared_ptrstd::unique_ptr 与自定义销毁器)。
  • 不易单元测试:全局状态难以重置。

3. 比较与实践建议

方案 线程安全性 成本 可维护性 适用场景
call_once + once_flag 需要显式控制初始化与销毁
局部静态(Meyer’s) 极低 只需一次构造,销毁无关紧要
  • 多线程竞争激烈:优先使用 std::call_once,可以在需要时再做销毁控制。
  • 简单工具类:局部静态即可,代码更简洁。

4. 小结

实现线程安全单例最推荐的方式是利用 C++11 标准库的 std::call_oncestd::once_flag,它既保证了单次初始化,又避免了显式加锁的复杂性。若项目对销毁顺序无特别需求,局部静态变量(Meyer’s Singleton)也是一种极简且高效的选择。无论采用哪种方式,都需要注意对内部成员的访问同步,避免在单例方法之外出现竞争。

发表评论