C++ 中实现线程安全的懒加载单例模式

在 C++ 中,单例模式经常用于需要全局共享资源的场景,例如日志系统、配置管理器或数据库连接池。实现线程安全的懒加载(即首次使用时才创建实例)单例模式,常见的方法有:

  1. C++11 本地静态变量

    class Logger {
    public:
        static Logger& instance() {
            static Logger instance;   // C++11 之后的实现保证线程安全
            return instance;
        }
        void log(const std::string& msg) { /* ... */ }
    
    private:
        Logger() = default;
        Logger(const Logger&) = delete;
        Logger& operator=(const Logger&) = delete;
    };

    C++11 标准规定,对同一块代码块内的 static 变量初始化是互斥的,适合大多数情况。

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

    class Singleton {
    public:
        static Singleton* getInstance() {
            if (!instance_) {                    // 第一检查
                std::lock_guard<std::mutex> lock(mutex_);
                if (!instance_) {                // 第二检查
                    instance_ = new Singleton();
                }
            }
            return instance_;
        }
        // 其它成员函数
    private:
        Singleton() = default;
        static Singleton* instance_;
        static std::mutex mutex_;
    };
    
    Singleton* Singleton::instance_ = nullptr;
    std::mutex Singleton::mutex_;

    该实现需要保证 instance_ 的原子性读写,C++11 起可以使用 std::atomic<Singleton*>。但如果不使用原子指针,可能出现微妙的竞态条件。

  3. Meyer’s Singleton 与 std::call_once

    class Config {
    public:
        static Config& get() {
            std::call_once(flag_, [](){ instance_ = new Config(); });
            return *instance_;
        }
    private:
        Config() = default;
        static Config* instance_;
        static std::once_flag flag_;
    };
    
    Config* Config::instance_ = nullptr;
    std::once_flag Config::flag_;

    std::call_once 在多线程环境下只会执行一次,保证初始化唯一。

  4. 懒加载的智能指针

    std::shared_ptr <Singleton> Singleton::getInstance() {
        std::call_once(flag_, [](){
            instance_ = std::make_shared <Singleton>();
        });
        return instance_;
    }

    通过 std::shared_ptr 方便管理生命周期,尤其是在多线程退出时能自动销毁。

注意事项

  • 销毁顺序:若使用静态局部对象,销毁顺序可能导致“静态初始化顺序问题”。如果程序在退出时需要优先销毁单例,建议手动调用析构或使用 std::unique_ptr 并在 atexit 注册析构。
  • 异常安全:若单例构造函数抛异常,std::call_once 会将异常重新抛出并允许下一次调用继续尝试,保证了安全。
  • 性能:C++11 的静态局部变量初始化已做了足够优化,除非存在极高并发场景,否则无需手动加锁。

总结来说,最推荐的实现方式是 C++11 的本地静态变量或 std::call_once,它们既简洁又可靠,满足绝大多数线程安全懒加载单例的需求。

发表评论