在现代 C++(C++11 及以后)中,std::call_once 和 std::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;
关键点说明
-
std::call_once- 只在第一次调用时执行传入的 lambda,随后所有线程直接跳过。
- 底层使用原子操作保证多线程访问时的可见性与序列化,避免了显式的
mutex锁开销。
-
std::once_flag- 与
call_once配合使用,标记是否已经初始化。 - 其内部实现为原子布尔值,不需要锁。
- 与
-
懒加载
- 单例实例仅在首次需要时才被创建,减少启动时的资源消耗。
-
内存模型
- 由于
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_once 与 std::once_flag 组合来实现线程安全的懒加载单例。相比手写锁或双重检查锁,代码更简洁、性能更优且易于维护。若项目已使用 C++11 或更高版本,只要包含 `