实现线程安全的单例模式(C++11)

在 C++11 及之后的标准中,线程安全的单例实现变得异常简洁。最常见的方法是使用 std::call_once 或者直接利用函数内部静态变量的懒初始化特性。下面我们分别介绍这两种方案,并说明它们的原理、优点与适用场景。

1. 用 std::call_oncestd::once_flag

#include <mutex>
#include <memory>

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(flag_, [](){
            ptr_.reset(new Singleton);
        });
        return *ptr_;
    }

    // 其他公共接口
    void doSomething() { /* ... */ }

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

    static std::once_flag flag_;
    static std::unique_ptr <Singleton> ptr_;
};

std::once_flag Singleton::flag_;
std::unique_ptr <Singleton> Singleton::ptr_;

优点

  • 对多线程访问时,保证单例对象只会被构造一次。
  • std::call_once 内部实现了必要的同步原语,使用更安全。

缺点

  • 对象销毁时,ptr_ 会在 main 结束时自动析构,除非你自己手动控制生命周期。

2. 用函数内部静态变量(C++11 之“线程安全的局部静态变量”)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance; // C++11 guarantees thread-safe initialization
        return instance;
    }

    void doSomething() { /* ... */ }

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

优点

  • 代码最简洁。
  • 语言层面保证线程安全。
  • 对象的析构顺序与 main 函数结束时全局对象的析构一致。

缺点

  • 若程序在构造期间需要访问 Singleton,但此时单例尚未被构造,可能导致未定义行为。
  • 在某些老旧编译器(C++03)中不保证线程安全。

3. 什么时候选择哪种实现?

场景 推荐实现 说明
需要手动控制销毁时机(如在 atexit 之前销毁) std::call_once + std::unique_ptr 可以在需要时手动释放资源
简单单例,程序生命周期结束时即可销毁 函数内部静态变量 代码简洁,符合 C++11 标准
需要在多线程环境中保证首次初始化的绝对安全 std::call_once 兼容 C++11 之前的实现,或者对更细粒度控制有需求

4. 进阶:延迟加载与多线程环境下的性能考量

虽然 std::call_once 和静态局部变量都能保证线程安全,但在极高并发的场景下,初始化期间的锁竞争仍然是不可避免的。若单例对象构造成本极高,可考虑 双检锁(Double-Check Locking)方案,但需要注意内存屏障和编译器优化。C++11 标准下可以通过 std::atomicstd::memory_order 明确控制。

class LazySingleton {
public:
    static LazySingleton* instance() {
        LazySingleton* 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 LazySingleton;
                instance_.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }

private:
    LazySingleton() = default;
    static std::atomic<LazySingleton*> instance_;
    static std::mutex mutex_;
};

std::atomic<LazySingleton*> LazySingleton::instance_{nullptr};
std::mutex LazySingleton::mutex_;

注意

  • 必须保证 new 之后的对象构造在 store 之前完成。
  • 需要在程序结束时手动 delete 对象,或者使用 std::unique_ptr 管理。

5. 小结

  • C++11 提供了两种最常用的线程安全单例实现:std::call_once 与函数内部静态变量。
  • 对于绝大多数应用,静态局部变量已经足够且最易维护。
  • 若需手动控制生命周期或在旧编译器下兼容,可选择 std::call_once 或双检锁实现。

掌握这两种模式后,你可以根据具体项目需求,灵活选择最合适的实现方式,实现既安全又高效的单例模式。

发表评论