在多线程环境下,确保单例对象只被创建一次且在任何线程中都能安全访问,是一个常见但细节繁琐的任务。下面将从 C++11 起支持的标准特性出发,介绍几种既安全又高效的实现方式,并讨论其优缺点。
1. 经典懒汉式 + std::call_once
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, []{
ptr_ = new Singleton();
});
return *ptr_;
}
// 其他成员函数...
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::once_flag flag_;
static Singleton* ptr_;
};
std::once_flag Singleton::flag_;
Singleton* Singleton::ptr_ = nullptr;
优点
- 延迟初始化:真正需要时才创建实例。
- 线程安全:
std::call_once保证即使多个线程同时调用instance(),只会有一次调用其内部 lambda。 - 无锁:
std::call_once在内部使用了高效的硬件原语。
缺点
- 对象在程序结束时不一定被析构(单例持久化)。如果需要在退出时清理,可在
atexit()注册析构函数或使用std::unique_ptr并配合std::atexit。
2. 局部静态变量(Meyers’ Singleton)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 之后保证线程安全
return instance;
}
// ...
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
优点
- 简洁:只需一句
static声明。 - 线程安全:C++11 起编译器保证局部静态变量的初始化是线程安全的。
- 自动析构:程序结束时
instance会被自动销毁。
缺点
- 初始化顺序未定义:如果在构造函数中使用了其他全局对象,可能导致“静态初始化顺序问题”。
- 销毁时机不可控:若在
main()结束前访问,可能已被销毁导致悬垂指针。
3. 带有锁的双检锁(Double-Checked Locking)
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
Singleton* tmp = instance_;
if (!tmp) {
std::lock_guard<std::mutex> lock(mutex_);
tmp = instance_;
if (!tmp) {
tmp = new Singleton();
instance_ = tmp;
}
}
return *tmp;
}
// ...
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
优点
- 性能:第一次实例化后后续访问不需要加锁。
- 延迟创建:与
call_once类似。
缺点
- 易错:必须保证
instance_的写操作对所有线程可见,使用std::atomic<Singleton*>或volatile。否则可能出现指令重排导致的未初始化对象泄漏。 - 实现复杂:相比前两种实现,代码更繁琐。
4. C++17 的 inline 变量 + std::once_flag
如果你使用 C++17 或更高版本,可以将 std::once_flag 和指针声明为 inline,进一步简化。
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, []{ ptr_ = new Singleton(); });
return *ptr_;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
inline static std::once_flag flag_;
inline static Singleton* ptr_ = nullptr;
};
优点
- 声明与定义合一:不需要在 .cpp 文件中再次定义静态成员。
- 保持线程安全:同
call_once的实现。
5. 什么时候选哪种?
| 场景 | 推荐实现 | 说明 |
|---|---|---|
| 需要最小代码量 | Meyers’ Singleton | 简洁、自动析构 |
| 需要显式销毁或定时释放 | call_once + std::unique_ptr |
手动控制生命周期 |
| 需要在全局初始化前使用 | call_once + 静态指针 |
避免静态初始化顺序问题 |
| 性能极限要求(后续访问不加锁) | 双检锁(但需注意原子) | 复杂度最高,易错 |
6. 小结
- C++11 以后,局部静态变量的初始化已变得线程安全,Meyers’ Singleton 成为最简洁的选择。
- 对于更细粒度的控制,
std::call_once提供了安全且高效的“一次性初始化”机制。 - 双检锁虽然理论上能减少锁开销,但实现细节繁琐,除非确有性能瓶颈且经验足够丰富,否则不建议使用。
通过合理选择实现方式,可在多线程 C++ 项目中轻松使用单例模式,而不必担心并发安全问题。祝编码愉快!