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

在多线程环境下,单例模式(Singleton)需要确保只有一个实例被创建,并且在任何线程访问时都能获得同一实例。下面以 C++17 为例,演示几种常见的实现方式,并说明它们的优缺点。

  1. C++11 的局部静态变量(Meyers’ Singleton)

    class Singleton {
    public:
        static Singleton& instance() {
            static Singleton instance;   // 第一次调用时初始化
            return instance;
        }
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
    private:
        Singleton() = default;
    };
    • 优点:编译器保证线程安全的局部静态初始化(C++11 起)。代码简洁,易于维护。
    • 缺点:在实例销毁时,如果其他线程仍在使用实例,可能导致未定义行为;且无法控制实例销毁时机(通常在程序退出时自动销毁)。
  2. 带双重检查锁(Double-Check Locking)

    #include <atomic>
    #include <mutex>
    
    class Singleton {
    public:
        static Singleton& instance() {
            Singleton* tmp = instance_.load(std::memory_order_acquire);
            if (!tmp) {
                std::lock_guard<std::mutex> lock(mutex_);
                tmp = instance_.load(std::memory_order_relaxed);
                if (!tmp) {
                    tmp = new Singleton;
                    instance_.store(tmp, std::memory_order_release);
                }
            }
            return *tmp;
        }
        // 其余部分与上面相同
    private:
        Singleton() = default;
        static std::atomic<Singleton*> instance_;
        static std::mutex mutex_;
    };
    
    std::atomic<Singleton*> Singleton::instance_{nullptr};
    std::mutex Singleton::mutex_;
    • 优点:延迟实例化且在首次调用前不占用锁。
    • 缺点:实现复杂,错误易产生。需要使用 std::atomic 和适当的内存顺序,否则仍可能出现竞态。
  3. 使用 std::call_oncestd::once_flag

    #include <mutex>
    
    class Singleton {
    public:
        static Singleton& instance() {
            std::call_once(flag_, [](){ instance_.reset(new Singleton); });
            return *instance_;
        }
        // 其余与第一种相同
    private:
        Singleton() = default;
        static std::unique_ptr <Singleton> instance_;
        static std::once_flag flag_;
    };
    
    std::unique_ptr <Singleton> Singleton::instance_;
    std::once_flag Singleton::flag_;
    • 优点:线程安全的单一初始化,代码可读性好,适合需要在运行时决定是否实例化的场景。
    • 缺点:与第一个实现类似,销毁时机不易控制。
  4. 惰性销毁(Lazy Destruction)
    如果你想在程序结束时不必担心销毁顺序,可以使用 std::shared_ptr 配合 std::weak_ptr

    class Singleton {
    public:
        static std::shared_ptr <Singleton> instance() {
            static std::weak_ptr <Singleton> weak_instance;
            std::shared_ptr <Singleton> strong_instance = weak_instance.lock();
            if (!strong_instance) {
                strong_instance = std::make_shared <Singleton>();
                weak_instance = strong_instance;
            }
            return strong_instance;
        }
    private:
        Singleton() = default;
    };
    • 优点:实例在最后一个 shared_ptr 被销毁时释放,避免了全局析构顺序问题。
    • 缺点:需要每次访问返回 std::shared_ptr,如果频繁调用会产生轻微开销。
  5. C++20 的 std::atomic<std::shared_ptr<T>>
    对于需要共享单例对象且线程安全的读写,C++20 提供了原子化 shared_ptr

    class Singleton {
    public:
        static std::shared_ptr <Singleton> instance() {
            std::shared_ptr <Singleton> ptr = instance_.load(std::memory_order_acquire);
            if (!ptr) {
                std::shared_ptr <Singleton> new_ptr = std::make_shared<Singleton>();
                if (instance_.compare_exchange_strong(ptr, new_ptr,
                                                     std::memory_order_release,
                                                     std::memory_order_relaxed)) {
                    ptr = new_ptr;
                }
            }
            return ptr;
        }
    private:
        Singleton() = default;
        static std::atomic<std::shared_ptr<Singleton>> instance_;
    };
    
    std::atomic<std::shared_ptr<Singleton>> Singleton::instance_;
    • 优点:原子操作保证多线程安全,且无需显式锁。
    • 缺点:依赖 C++20,可能与旧编译器不兼容。

小结

  • 最推荐:C++11 局部静态变量(Meyers’ Singleton)——实现简单,编译器保证线程安全。
  • 若需更细粒度控制std::call_oncestd::once_flag
  • 若担心销毁顺序:使用 std::shared_ptrstd::weak_ptr
  • 多线程读写共享实例:C++20 原子化 shared_ptr

在实际项目中,往往把单例设计成 “延迟初始化 + 线程安全” 的形式,并在构造函数中完成必要的资源准备。需要注意的是,单例模式并非万能,若滥用会导致全局状态污染、单元测试困难以及并发死锁风险。使用时请结合项目实际需求和团队经验进行选择。

发表评论