在 C++17 之前,实现线程安全单例通常需要手动使用互斥锁或双重检查锁定(double‑checked locking)来避免多线程环境下的竞争。自 C++11 起,标准库提供了原子类型和 std::call_once 等工具,使得实现线程安全单例变得更加简洁可靠。本文将展示一种利用 std::call_once 的现代实现方法,并对比传统方法,帮助读者快速掌握。
1. 传统实现(不推荐)
class Singleton {
public:
static Singleton& instance() {
if (!m_instance) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_instance) { // 双重检查
m_instance = new Singleton();
}
}
return *m_instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* m_instance;
static std::mutex m_mutex;
};
Singleton* Singleton::m_instance = nullptr;
std::mutex Singleton::m_mutex;
此实现虽然工作,但存在多处潜在问题:
- 性能损耗:每次访问都需要判断指针是否为空,虽然大多数情况下跳过锁,但仍有一次不必要的检查。
- 可读性差:双重检查锁定模式在某些平台上容易出错,需要确保
m_instance的写入是原子的。 - 资源管理:手动
new/delete需要在合适时机释放,容易产生内存泄漏或悬空指针。
2. C++17 现代实现(推荐)
利用 std::call_once 和 std::once_flag 可以让初始化只执行一次,并且完全线程安全。
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, []() { instancePtr.reset(new Singleton); });
return *instancePtr;
}
// 公开的业务接口示例
void do_something() const { /* ... */ }
private:
Singleton() = default;
~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 = nullptr;
std::once_flag Singleton::initFlag;
为什么这么好?
| 特性 | 说明 |
|---|---|
| 一次性执行 | std::call_once 确保无论多少线程并发调用,闭包只会执行一次。 |
| 延迟初始化 | 对象真正创建时才会发生,避免不必要的资源占用。 |
| 无锁(实现细节) | 标准库实现通常使用轻量级原子操作或内部锁,效率高且安全。 |
| 自动销毁 | 使用 std::unique_ptr,程序结束时自动析构,避免泄漏。 |
| 易读易维护 | 代码简洁,逻辑明确。 |
3. 进一步简化(C++20 的 std::once_flag 结合 lambda)
如果你使用 C++20 或更高版本,还可以把 instancePtr 和 initFlag 放进一个私有结构中,甚至使用 inline static 直接初始化:
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 之中的“静态局部变量”已线程安全
return instance;
}
private:
Singleton() = default;
// 其余声明同上
};
注意:此处的静态局部变量在 C++11 之后已经保证线程安全,简化了实现。然而,若需要显式控制初始化顺序或在多文件间共享实例,使用 std::call_once 仍然更为稳妥。
4. 小结
- 最佳实践:在 C++17 及以后,首选
std::call_once+std::unique_ptr或直接使用线程安全的静态局部变量。 - 避免双重检查锁定:它容易出错且不必要。
- 保持单例接口简洁:提供必要的业务方法,避免在单例内部暴露过多实现细节。
通过上述方法,你可以在任何 C++17 程序中安全、轻松地实现单例模式,并在多线程环境下保持性能与安全性的最佳平衡。