在多线程环境下实现一个线程安全的单例模式,最常见的做法是使用 C++11 引入的局部静态变量初始化以及 std::call_once。下面分别介绍这两种方法,并给出完整可编译的代码示例。
1. C++11 的局部静态变量初始化
C++11 标准保证了对局部静态对象的初始化是线程安全的。只需要把单例的实例放在一个函数内部的静态局部变量即可。
#include <iostream>
#include <mutex>
class Singleton {
public:
// 获取单例实例
static Singleton& instance() {
static Singleton instance; // C++11 保证线程安全
return instance;
}
// 禁止复制和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
void doSomething() { std::cout << "Doing something in thread " << std::this_thread::get_id() << '\n'; }
private:
Singleton() { std::cout << "Singleton constructed\n"; }
~Singleton() { std::cout << "Singleton destructed\n"; }
};
int main() {
// 创建若干线程同时访问单例
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back([]{
Singleton::instance().doSomething();
});
}
for (auto& t : threads) t.join();
return 0;
}
关键点
static Singleton instance;在第一次调用instance()时初始化,后续调用直接返回同一实例。- 标准保证了初始化过程中的“单一写入”与“可见性”,因此不需要显式的互斥锁。
- 通过删除拷贝构造、赋值操作等,防止了单例被复制。
2. 使用 std::call_once
如果你想要更细粒度地控制初始化过程,或者在旧编译器(C++11 之前)上工作,可以使用 std::call_once。
#include <iostream>
#include <mutex>
#include <thread>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, []{
instancePtr = new Singleton();
});
return *instancePtr;
}
void doSomething() {
std::cout << "Doing something in thread " << std::this_thread::get_id() << '\n';
}
private:
Singleton() { std::cout << "Singleton constructed\n"; }
~Singleton() { std::cout << "Singleton destructed\n"; }
static Singleton* instancePtr;
static std::once_flag initFlag;
};
// 需要在 cpp 文件中定义静态成员
Singleton* Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back([]{
Singleton::instance().doSomething();
});
}
for (auto& t : threads) t.join();
return 0;
}
关键点
std::call_once确保 lambda 只被执行一次,即使多个线程同时调用。std::once_flag必须是static,且与call_once配合使用。- 需要手动管理单例指针的生命周期,或者使用
std::unique_ptr自动析构。
3. 对比与选择
| 方法 | 适用编译器 | 代码简洁度 | 线程安全实现方式 |
|---|---|---|---|
| 局部静态变量 | C++11 及以上 | 最简洁 | 编译器内部实现 |
std::call_once |
C++11 及以上 | 稍复杂 | 通过 once_flag 与 call_once 保障 |
- 若项目已使用 C++11 或更高版本,优先选择局部静态变量,因为代码更短且不需要额外的同步对象。
- 若想在旧编译器上兼容,或者需要在单例初始化时执行更复杂的逻辑,
std::call_once更灵活。
4. 进一步考虑
- 懒加载 vs 预初始化:局部静态变量是在第一次访问时才初始化,满足懒加载需求。若需要在程序启动时就创建单例,可在
main入口处显式调用一次Singleton::instance()。 - 销毁顺序:使用局部静态变量时,实例会在程序退出时按逆序析构;使用
std::call_once创建的裸指针需要自行释放,建议使用std::unique_ptr或std::shared_ptr。 - 异常安全:如果单例构造函数抛异常,
std::call_once需要重新执行;局部静态变量在抛异常后会再次尝试初始化,符合规范。
结语
线程安全的单例模式是多线程 C++ 开发中的常见需求。C++11 的特性大大简化了实现:局部静态变量初始化即可保证线程安全;若需要更灵活的初始化策略,std::call_once 是可靠的选择。根据项目的编译器版本和具体需求,选择合适的方法即可实现安全、简洁的单例。