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

在 C++11 标准以后,标准库提供了更安全、更简洁的单例实现方法。
下面我们先讨论传统的双检锁实现与其缺陷,然后给出基于 std::call_once 的最佳实践。

1. 传统双检锁(Double-Checked Locking)

class Singleton {
private:
    static Singleton* instance;
    static std::mutex mtx;
    Singleton() = default;

public:
    static Singleton* getInstance() {
        if (!instance) {                     // 第一次检查
            std::lock_guard<std::mutex> lock(mtx);
            if (!instance) {                 // 第二次检查
                instance = new Singleton();
            }
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

问题

  • 内存可见性:在多线程环境下,写操作 instance = new Singleton() 可能在其他线程看到之前,完成构造函数的部分工作。
  • 指令重排:编译器或 CPU 可能会把对象构造的步骤与指针赋值顺序打乱。
  • 性能瓶颈:每次调用都需要检查 instance,即使对象已经存在,也会有额外的判断开销。

2. 使用 std::call_oncestd::once_flag

C++11 标准提供了 std::call_once 机制,确保某个函数只被执行一次,且对所有线程可见。

class Singleton {
private:
    Singleton() = default;
    static Singleton* instance;
    static std::once_flag initFlag;

    static void initSingleton() {
        instance = new Singleton();
    }

public:
    static Singleton* getInstance() {
        std::call_once(initFlag, initSingleton);
        return instance;
    }
};

Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::initFlag;

优点

  • 简洁安全call_once 内部使用原子操作,避免了双检锁带来的可见性问题。
  • 延迟加载:仅在第一次请求时创建实例,符合懒汉式单例。
  • 可扩展:如果你想把初始化逻辑放到 initSingleton 中,保持线程安全。

3. 现代 C++:局部静态变量(Meyers 单例)

最简洁且线程安全的实现是利用 C++11 对局部静态变量的初始化是线程安全的特性:

class Singleton {
private:
    Singleton() = default;
public:
    static Singleton& instance() {
        static Singleton inst; // C++11 起线程安全
        return inst;
    }
};

优点:

  • 无显式锁:编译器自动处理同步。
  • 资源释放inst 会在程序结束时析构,符合 RAII。
  • 使用更直观:返回引用,避免了指针操作。

4. 何时选择哪种实现?

场景 推荐实现
需要在单例中执行复杂初始化逻辑,且想把初始化拆成多个步骤 std::call_once
只需要一个极简实现,且不想自己写锁 局部静态变量(Meyers)
兼容 C++11 前的编译器 双检锁(但要注意实现细节)

5. 小结

C++11 之后,单例模式的实现变得更安全、更简洁。

  • std::call_once 提供了线程安全的显式初始化方式。
  • Meyers 单例(局部静态变量)是最简洁、易维护的选择。

在实际项目中,除非有特殊需求,一般推荐使用局部静态变量实现,保持代码简洁且线程安全。

发表评论