在 C++ 设计模式中,单例(Singleton)是一种常见且有用的模式,用于确保一个类只有一个实例,并提供全局访问点。随着多线程编程的普及,传统单例实现容易产生线程安全问题。下面介绍几种现代 C++(C++11 及以后)中实现线程安全单例的常用方法,并分析其优缺点。
1. 局部静态变量(Meyers 单例)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 保证线程安全
return instance;
}
// 其他成员函数...
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
优点
- 简单:只需一行代码即可完成线程安全的初始化。
- 延迟加载:对象在第一次调用
instance()时才被创建,避免不必要的开销。 - 内存泄漏避免:实例随程序结束而自动销毁,无需手动释放。
缺点
- 无法延迟销毁:对象会在程序退出时销毁,若需要在特定时机销毁则不适用。
- 难以测试:静态对象难以被替换,导致单元测试困难。
- 初始化顺序不确定:若在多线程环境下其他静态对象的初始化与单例交叉,可能导致初始化顺序问题(C++ 标准并未完全保证)。
2. std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, []() {
instancePtr.reset(new Singleton);
});
return *instancePtr;
}
// ...
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::unique_ptr <Singleton> instancePtr;
static std::once_flag initFlag;
};
std::unique_ptr <Singleton> Singleton::instancePtr;
std::once_flag Singleton::initFlag;
优点
- 显式控制:可在需要时手动销毁或重置单例。
- 兼容多线程:
std::call_once在 C++11 之后已保证线程安全。 - 更易测试:可以在测试中重置
instancePtr。
缺点
- 实现稍显繁琐:需要额外的指针、once_flag 等成员。
- 延迟销毁:需要手动调用销毁函数,使用不当可能导致资源泄漏。
3. 双重检查锁定(Double-Checked Locking, DCL)
class Singleton {
public:
static Singleton* instance() {
Singleton* tmp = instancePtr.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mutex_);
tmp = instancePtr.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton;
instancePtr.store(tmp, std::memory_order_release);
}
}
return tmp;
}
// ...
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::atomic<Singleton*> instancePtr;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instancePtr{nullptr};
std::mutex Singleton::mutex_;
优点
- 延迟初始化:在第一次访问时才创建实例。
- 性能:在已初始化之后,获取实例不需要锁,开销极小。
缺点
- 实现复杂:需要仔细使用原子操作与内存序。
- 可移植性:早期 C++11 编译器在实现细节上可能存在差异。
- 容易出错:错误的内存序会导致“懒汉式”实例化不安全。
4. C++20 consteval 结合 constexpr(静态局部变量已足够)
从 C++20 开始,consteval 和 constexpr 的使用可以让单例的构造在编译期完成,进一步提升安全性和性能。但这通常仅适用于无状态或只读单例。
小结
| 方法 | 代码量 | 线程安全 | 延迟销毁 | 适用场景 |
|---|---|---|---|---|
| 局部静态变量 | 少 | ✅ (C++11+) | ❌ | 小型项目、只需要单例且不需要手动销毁 |
std::call_once |
中 | ✅ | ✅ (手动) | 需要手动销毁或在测试中重置 |
| DCL | 多 | ✅ (正确实现) | ✅ | 性能敏感且需要手动销毁 |
| C++20 constexpr/consteval | 少 | ✅ | ❌ | 只读、编译期初始化 |
实战建议
- 对大多数应用而言,局部静态变量(Meyers 单例)已足够且最安全。
- 若你需要在单元测试或多次运行期间重置单例,请使用
std::call_once。 - 对性能极致要求且对实现细节掌控得足够好的场景,可考虑 DCL,但请谨慎使用。
结语
C++ 的多线程机制与单例模式的结合并不复杂,关键在于利用语言提供的线程安全原语(如 std::call_once、原子操作等)实现简洁、可维护的代码。希望以上介绍能帮助你在项目中正确、高效地实现线程安全的单例。