在 C++17 之前,常见的单例实现方式有三种:懒汉式(双重检查锁定)、饿汉式、Meyers 单例。每种方式都有自己的优缺点,尤其在多线程环境下的安全性更是重要。下面我们将逐步演示如何在 C++17 中使用 std::call_once 与 std::once_flag 结合 std::unique_ptr,实现一个线程安全、懒加载的单例。
1. 设计思路
- 懒加载:实例只在第一次需要时创建,节省资源。
- 线程安全:使用
std::call_once确保实例只被创建一次,即使多个线程同时请求。 - 资源释放:使用
std::unique_ptr自动管理生命周期,避免手动delete。
2. 代码实现
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
#include <chrono>
class Singleton {
public:
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() {
// std::call_once 保证只执行一次初始化
std::call_once(initFlag_, []() {
instance_ = std::unique_ptr <Singleton>(new Singleton());
});
return *instance_;
}
void doWork() {
std::cout << "线程 " << std::this_thread::get_id() << " 正在使用单例实例,地址: " << this << "\n";
}
private:
Singleton() {
std::cout << "Singleton 构造函数调用,地址: " << this << "\n";
}
~Singleton() {
std::cout << "Singleton 析构函数调用,地址: " << this << "\n";
}
static std::unique_ptr <Singleton> instance_;
static std::once_flag initFlag_;
};
std::unique_ptr <Singleton> Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
3. 多线程测试
void worker() {
// 随机延迟模拟并发情况
std::this_thread::sleep_for(std::chrono::milliseconds(rand() % 100));
Singleton::getInstance().doWork();
}
int main() {
std::srand(static_cast <unsigned>(std::time(nullptr)));
std::vector<std::thread> threads;
// 创建 10 个线程同时请求单例
for (int i = 0; i < 10; ++i) {
threads.emplace_back(worker);
}
for (auto& t : threads) {
t.join();
}
std::cout << "所有线程已结束。\n";
return 0;
}
4. 运行结果(示例)
Singleton 构造函数调用,地址: 0x55f8c0b5c2e0
线程 140355345816448 正在使用单例实例,地址: 0x55f8c0b5c2e0
线程 140355337423744 正在使用单例实例,地址: 0x55f8c0b5c2e0
...
所有线程已结束。
Singleton 析构函数调用,地址: 0x55f8c0b5c2e0
可以看到:
- 构造函数仅被调用一次,地址一致;
- 所有线程共享同一实例;
- 在程序退出时,单例被正确析构。
5. 关键点总结
| 技术 | 作用 | 说明 |
|---|---|---|
std::call_once |
保证单次初始化 | 线程安全地执行一次初始化代码 |
std::once_flag |
伴随 call_once 使用 |
记录初始化状态 |
std::unique_ptr |
自动释放 | 避免手动 delete |
delete 拷贝构造/赋值 |
防止多实例 | 确保唯一实例 |
6. 进一步扩展
- 懒加载与自毁:如果想在程序结束后显式销毁单例,可以使用
std::shared_ptr与std::weak_ptr,或在Singleton的析构中注册std::atexit进行清理。 - 模板单例:把
Singleton设计成模板类 `Singleton `,适用于多种对象。 - 延迟初始化的对象:若单例内部资源本身昂贵,可以再使用
std::optional或std::shared_ptr延迟加载。
通过上述实现,你可以在任何 C++17 项目中快速、安全地使用单例模式,满足多线程并发访问的需求。