在多线程环境下,单例模式需要确保只有一个实例被创建,并且在并发访问时不会出现竞争条件。C++17 以后可以利用 std::call_once 和 std::once_flag 来实现最简单、最安全的单例。下面从设计思路、实现细节和性能考量三个角度,逐步阐述如何编写一个线程安全的单例。
1. 设计思路
- 懒汉式(Lazy)
- 只在第一次使用时创建实例,避免不必要的初始化成本。
- 线程安全
- 使用
std::call_once确保初始化只执行一次。
- 使用
- 避免“双重检查锁定”
- 双重检查锁定(Double-Check Locking)在某些编译器/硬件上仍可能产生数据竞争。
- 保证对象在整个程序生命周期内有效
- 单例实例应在程序结束前保持存在,或使用
std::shared_ptr与自定义删除器管理生命周期。
- 单例实例应在程序结束前保持存在,或使用
2. 实现代码
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>
class ThreadSafeSingleton {
public:
// 提供全局访问点
static ThreadSafeSingleton& Instance() {
std::call_once(initFlag_, &ThreadSafeSingleton::Init);
return *instance_;
}
// 禁止复制与移动
ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;
ThreadSafeSingleton(ThreadSafeSingleton&&) = delete;
ThreadSafeSingleton& operator=(ThreadSafeSingleton&&) = delete;
// 示例业务函数
void DoWork() {
std::cout << "Thread " << std::this_thread::get_id() << " is using singleton at address " << this << "\n";
}
private:
ThreadSafeSingleton() { std::cout << "Singleton constructed\n"; }
~ThreadSafeSingleton() { std::cout << "Singleton destroyed\n"; }
static void Init() {
instance_ = std::unique_ptr <ThreadSafeSingleton>(new ThreadSafeSingleton);
}
static std::once_flag initFlag_;
static std::unique_ptr <ThreadSafeSingleton> instance_;
};
// 静态成员定义
std::once_flag ThreadSafeSingleton::initFlag_;
std::unique_ptr <ThreadSafeSingleton> ThreadSafeSingleton::instance_ = nullptr;
// 简单测试
int main() {
constexpr int threadCount = 10;
std::vector<std::thread> workers;
for (int i = 0; i < threadCount; ++i) {
workers.emplace_back([]{
ThreadSafeSingleton::Instance().DoWork();
});
}
for (auto& t : workers) t.join();
return 0;
}
说明:
std::call_once与std::once_flag保证Init()只会被调用一次,所有线程在第一次访问Instance()时会阻塞直到实例完成初始化。- 由于使用
unique_ptr,单例在程序退出时会被正确析构。若需要更细粒度的控制(例如懒销毁),可以改用shared_ptr或手动管理析构。 Instance()返回引用,调用者无需担心内存泄漏。
3. 性能与可读性评估
| 方案 | 初始化开销 | 访问开销 | 可读性 | 线程安全 | 适用场景 |
|---|---|---|---|---|---|
static local(C++11) |
低(只需一次) | 低(无锁) | 高 | 通过编译器实现 | 轻量级 |
std::call_once |
低(只一次) | 低(无锁) | 中 | 高 | 需要显式控制 |
double-checked locking |
低 | 低(锁粒度小) | 低 | 需细心 | 避免 call_once 的实现细节 |
- static local:最简洁,C++11 标准保证线程安全。
- call_once:更直观地表明“只调用一次”,适合需要手动管理生命周期或需要在特定时机初始化的场景。
4. 常见错误与陷阱
| 错误 | 说明 | 解决方案 |
|---|---|---|
| 在构造函数里调用单例 | 可能导致递归调用 | 避免在构造函数里访问 Instance() |
| 使用裸指针 | 可能导致悬挂指针 | 使用 unique_ptr 或 shared_ptr |
| 忘记删除拷贝构造/赋值 | 可能导致多个实例 | delete 拷贝构造/赋值运算符 |
| 多次包含头文件导致重复定义 | 链接错误 | 使用 include guards 或 #pragma once |
5. 小结
std::call_once是 C++ 中最推荐的线程安全单例实现方式。- 对于大多数场景,
static local(C++11 及以后)足够简洁且安全;如果需要更细粒度的控制或在类外初始化,则call_once是更好的选择。 - 记住禁用拷贝/移动构造和赋值运算符,确保单例的唯一性。
通过上述实现,你可以在任何多线程 C++ 应用中安全、可靠地使用单例模式,而不必担心竞争条件或初始化问题。