在现代C++(C++11及以后)中,实现一个线程安全的单例模式非常直接。关键是利用语言层面对静态局部变量初始化的保证,以及std::call_once与std::once_flag的结合。下面给出一个完整的实现示例,并对其工作原理做详细说明。
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
class Singleton {
public:
// 提供获取实例的静态方法
static Singleton& Instance() {
// 线程安全的静态局部变量
static Singleton instance;
return instance;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 示例功能
void DoWork(int thread_id) {
std::lock_guard<std::mutex> lock(io_mutex_);
std::cout << "Thread " << thread_id << " 使用单例实例进行工作\n";
}
private:
Singleton() {
std::cout << "Singleton 构造函数被调用\n";
}
~Singleton() = default;
std::mutex io_mutex_;
};
void Worker(int id) {
Singleton::Instance().DoWork(id);
}
int main() {
const int kThreadCount = 5;
std::thread threads[kThreadCount];
for (int i = 0; i < kThreadCount; ++i) {
threads[i] = std::thread(Worker, i);
}
for (auto& th : threads) {
th.join();
}
return 0;
}
关键点解析
-
静态局部变量
static Singleton instance;根据C++11标准,局部静态变量的初始化是线程安全的。也就是说,即使多个线程同时进入
Instance()函数,编译器会保证只有一个线程完成实例化,其余线程会等待初始化完成后再使用同一个实例。 -
std::call_once与std::once_flag(可选实现)
如果你想手动控制初始化过程,可以使用std::call_once。它同样提供线程安全的一次性执行机制,但对大多数场景来说,静态局部变量已足够。class Singleton { public: static Singleton& Instance() { std::call_once(init_flag_, [](){ instance_.reset(new Singleton); }); return *instance_; } private: static std::once_flag init_flag_; static std::unique_ptr <Singleton> instance_; }; -
禁止拷贝与赋值
为了保证单例对象只能有唯一实例,必须删除拷贝构造函数和拷贝赋值运算符。这样就算外部代码尝试拷贝,也会在编译阶段报错。 -
线程安全的业务逻辑
上述例子中,DoWork()使用了成员std::mutex io_mutex_来保护输出操作,防止多线程并发打印导致混乱。业务代码中的其他共享资源也应按需加锁或使用原子操作。 -
构造函数与析构函数
构造函数是私有的,确保外部不能直接创建实例。析构函数默认可见性足够,若需要在程序结束时执行清理,可在单例类中实现自定义析构或使用智能指针管理生命周期。
常见误区
- 懒汉式与饿汉式的混淆:饿汉式(在程序启动时即初始化)在多线程环境下需要手动加锁;懒汉式(延迟初始化)若使用静态局部变量即可天然线程安全。
- 双重检查锁(Double-Checked Locking):在C++11之前不安全,现代C++中推荐直接使用静态局部变量或
std::call_once。 - 多继承导致的实例化顺序:如果单例类继承自其他类,确保基类构造不涉及多线程共享资源,以免引发竞态。
结语
在C++11及以后版本,借助语言提供的线程安全特性,编写一个简洁、可靠的单例模式变得非常容易。只需关注核心的静态局部变量或std::call_once即可,避免过度包装导致的复杂性。这样既能满足高并发环境下的安全性,又能保持代码的可维护性。