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

在多线程环境下,单例模式的实现需要保证只有一个实例,并且实例的创建过程是线程安全的。C++20提供了几个特性,使得实现更加简洁、安全和高效。

  1. 使用std::call_oncestd::once_flag
    std::call_once 会在多线程调用时保证其内部函数只执行一次,而 std::once_flag 用于跟踪状态。结合 std::unique_ptr 可以实现懒加载单例,代码示例如下:

    #include <memory>
    #include <mutex>
    
    class Singleton {
    public:
        static Singleton& instance() {
            std::call_once(initFlag_, []() {
                instancePtr_ = std::unique_ptr <Singleton>(new Singleton());
            });
            return *instancePtr_;
        }
    
        // 禁止拷贝和移动
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
        Singleton(Singleton&&) = delete;
        Singleton& operator=(Singleton&&) = delete;
    
        void doSomething() { /* 业务逻辑 */ }
    
    private:
        Singleton() = default;
        ~Singleton() = default;
    
        static std::once_flag initFlag_;
        static std::unique_ptr <Singleton> instancePtr_;
    };
    
    std::once_flag Singleton::initFlag_;
    std::unique_ptr <Singleton> Singleton::instancePtr_ = nullptr;

    该实现的优点是:

    • 线程安全std::call_once 内部使用了原子操作,确保单例被安全创建。
    • 懒加载:只有在第一次调用 instance() 时才会实例化。
    • 避免双重检查锁定:传统的 double‑check lock 在 C++98/11 中存在可见性问题,而 std::call_once 已经内部完成了正确的同步。
  2. 使用 C++20 的 std::atomic<std::shared_ptr<>>
    如果需要支持多线程对单例的读写,并且希望实现更细粒度的锁控制,可以采用原子共享指针。示例:

    #include <memory>
    #include <atomic>
    
    class Singleton {
    public:
        static std::shared_ptr <Singleton> instance() {
            auto ptr = instance_.load(std::memory_order_acquire);
            if (!ptr) {
                std::lock_guard<std::mutex> lock(mutex_);
                ptr = instance_.load(std::memory_order_relaxed);
                if (!ptr) {
                    ptr = std::shared_ptr <Singleton>(new Singleton());
                    instance_.store(ptr, std::memory_order_release);
                }
            }
            return ptr;
        }
    
        void doSomething() { /* 业务逻辑 */ }
    
    private:
        Singleton() = default;
        ~Singleton() = default;
    
        static std::atomic<std::shared_ptr<Singleton>> instance_;
        static std::mutex mutex_;
    };
    
    std::atomic<std::shared_ptr<Singleton>> Singleton::instance_ = nullptr;
    std::mutex Singleton::mutex_;

    这里的实现兼顾了原子性与锁的使用,保证了实例创建的可见性,同时避免了不必要的锁竞争。

  3. 利用 C++20 的 std::atomic_refstd::shared_mutex
    对于需要频繁读取但偶尔写入的单例,std::shared_mutex 可以提供读写分离。结合 std::atomic_ref 可以进一步减少内存层面的竞争。代码略显复杂,但在高并发读场景下会有性能提升。

  4. 注意单例的销毁
    在多线程程序退出时,如果单例使用了 new 创建的裸指针,可能导致析构顺序问题。推荐使用 std::unique_ptrstd::shared_ptr 来管理生命周期,或者让单例成为函数内静态局部对象(C++11 之下已保证线程安全的构造):

    Singleton& Singleton::instance() {
        static Singleton instance;
        return instance;
    }

    这种写法最简洁且无锁,但无法在单例需要动态销毁或自定义内存管理时使用。

总结

  • std::call_oncestd::once_flag 是最简单、最安全的方式。
  • 对于需要细粒度控制或多线程读写并发的情况,可考虑 std::atomic<std::shared_ptr<>>std::shared_mutex
  • 始终关注实例销毁顺序和资源管理,避免内存泄漏或悬挂引用。

通过以上方法,你可以在 C++20 项目中安全、高效地实现单例模式,满足多线程环境下的需求。

发表评论