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

在多线程环境下,单例模式常用于控制全局资源的唯一实例。传统的实现方式(如懒汉式、饿汉式)在 C++11 之后可以利用语言级别的线程安全特性简化实现。下面介绍几种常见的线程安全单例实现,并比较它们的优缺点。


1. 局部静态变量(Meyers 单例)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;   // C++11 规定此初始化是线程安全的
        return instance;
    }

    // 其他接口
private:
    Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

优点

  • 代码简洁,几乎不需要额外的同步机制。
  • 只在第一次调用时创建实例,后续调用成本极低。
  • 适用于大多数情况,推荐使用。

缺点

  • 仅适用于 C++11 及以上。
  • 对于需要在程序关闭前手动销毁资源的场景(例如动态链接库卸载)不太友好,实例会在程序退出时自动析构。

2. 双重检查锁(Double-Check Locking,DCL)

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

Singleton* Singleton::ptr_ = nullptr;
std::mutex Singleton::mtx_;

优点

  • 对资源的访问比局部静态变量更灵活,可以自行控制对象的生命周期。
  • 在某些场景下(如需要在某一阶段销毁实例)更适用。

缺点

  • 代码稍显复杂,需要显式的锁和双重检查。
  • 需要注意指针的原子性,避免在多线程下出现指针不一致的情况(C++11 的 std::atomic 可以帮助解决)。

3. std::call_once 与 std::once_flag

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

Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;

优点

  • std::call_once 的语义非常明确,适用于一次性初始化。
  • 线程安全且性能良好,锁的持有时间极短。

缺点

  • 仍需要手动删除实例,若不手动删除会造成资源泄漏。

4. 智能指针 + std::shared_ptr

如果想在多线程环境中共享单例,同时在不再使用时自动销毁,可以结合 std::shared_ptrstd::call_once

class Singleton {
public:
    static std::shared_ptr <Singleton> instance() {
        std::call_once(flag_, []{
            instance_ = std::shared_ptr <Singleton>(new Singleton());
        });
        return instance_;
    }
private:
    Singleton() = default;
    static std::shared_ptr <Singleton> instance_;
    static std::once_flag flag_;
};

std::shared_ptr <Singleton> Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;

此时单例对象的生命周期由 std::shared_ptr 管理,使用完后会自动析构。


5. 何时使用哪种实现?

场景 推荐实现
简单且不需要手动销毁 Meyers 单例
需要手动销毁资源(如动态库) std::call_once + std::shared_ptr
需要自定义销毁时机 std::call_once + raw pointer
对于 C++11 之前的代码 双重检查锁(但需谨慎)

6. 常见错误

  1. 忘记删除拷贝构造/赋值操作
    Singleton 必须把拷贝构造函数和赋值操作符删除或设为 delete,否则可能产生多实例。

  2. 多线程未同步的写
    在双重检查锁或 std::call_once 之外,任何对实例的写操作都需要加锁。

  3. 静态变量在多线程环境下的销毁
    Meyers 单例 的实例在程序退出时会被销毁,若析构时依赖其他静态对象,可能导致顺序问题。可采用 std::atexit 注册自定义析构或使用智能指针。


7. 小结

在 C++11 及以后,最简洁且安全的单例实现就是局部静态变量(Meyers 单例)。如果需要更细粒度的控制生命周期或在多线程环境中动态销毁实例,std::call_oncestd::shared_ptr 是更好的选择。始终记住,单例模式虽然方便,但也要慎用,避免过度使用导致的耦合和难以测试的问题。

发表评论