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

在现代 C++(C++11 及以后)中,std::call_oncestd::once_flag 提供了一种轻量且线程安全的方式来实现懒加载的单例。与传统的 double-checked locking 方案相比,后者容易出现指令重排、内存可见性等问题,而 call_once 的实现已被各大编译器优化为原子操作,几乎不产生运行时开销。以下示例演示了最简洁的单例实现,并说明了其线程安全的原因。

#include <iostream>
#include <mutex>

class Singleton {
public:
    // 通过静态成员函数返回单例实例
    static Singleton& instance() {
        std::call_once(initFlag, []() { instancePtr = new Singleton; });
        return *instancePtr;
    }

    // 禁止拷贝构造和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    void doSomething() {
        std::cout << "Doing something in thread " << std::this_thread::get_id() << std::endl;
    }

private:
    Singleton() = default;                 // 私有构造函数
    ~Singleton() = default;                // 私有析构函数(如需在程序结束时自动销毁,需自行释放)

    static Singleton* instancePtr;
    static std::once_flag initFlag;
};

// 定义静态成员
Singleton* Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;

关键点说明

  1. std::call_once

    • 只在第一次调用时执行传入的 lambda,随后所有线程直接跳过。
    • 底层使用原子操作保证多线程访问时的可见性与序列化,避免了显式的 mutex 锁开销。
  2. std::once_flag

    • call_once 配合使用,标记是否已经初始化。
    • 其内部实现为原子布尔值,不需要锁。
  3. 懒加载

    • 单例实例仅在首次需要时才被创建,减少启动时的资源消耗。
  4. 内存模型

    • 由于 call_once 的实现遵循 C++ 内存模型中的“内存同步”语义,确保所有线程在获取到实例后看到完整初始化的对象。

对比传统双重检查锁(Double-Checked Locking)

// 非线程安全(示例)
Singleton* getInstance() {
    if (!instance) {                     // 第一次检查
        std::lock_guard<std::mutex> lock(mtx);
        if (!instance) {                 // 第二次检查
            instance = new Singleton;
        }
    }
    return instance;
}
  • 该方案容易因为编译器优化或 CPU 指令重排导致第二次检查时 instance 已被写入但尚未完成构造,从而产生数据竞争。
  • std::call_once 内部已经考虑了这些细节,编译器不会对其进行重排序。

使用示例

#include <thread>
#include <vector>

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back([]{
            Singleton::instance().doSomething();
        });
    }
    for (auto& t : threads) t.join();
    return 0;
}

运行上述程序时,无论线程调度如何,都会得到同一个 Singleton 实例,并且输出中所有线程都能看到相同的实例地址。

结语

在 C++20 及更高版本中,推荐使用 std::call_oncestd::once_flag 组合来实现线程安全的懒加载单例。相比手写锁或双重检查锁,代码更简洁、性能更优且易于维护。若项目已使用 C++11 或更高版本,只要包含 `

` 并遵循上述模式,即可获得最佳的线程安全保证。

发表评论