在 C++20 之前,单例的实现常常依赖于 Meyers 单例(局部静态变量)或手动双重检查锁定(double-checked locking)。然而,C++20 引入了更强大的并发工具,例如 std::atomic、std::mutex 和 std::call_once,以及更简洁的语法特性。下面给出一个完整的、线程安全、懒加载、易于使用的单例实现,并对关键点进行解释。
1. 基本思路
- 懒加载:单例对象仅在第一次访问时才创建,避免无谓的资源占用。
- 线程安全:在多线程环境下保证只有一个实例被创建,且后续访问直接返回该实例。
- 简洁易用:使用者仅需通过
Singleton::instance()获取引用,无需关心线程同步细节。
2. 代码实现
#include <iostream>
#include <mutex>
#include <memory>
class Singleton {
public:
// 获取单例引用
static Singleton& instance() {
// std::call_once 与 std::once_flag 结合,保证只执行一次初始化
std::call_once(initFlag_, []{
// 使用 std::unique_ptr 以确保析构时自动销毁
instancePtr_ = std::unique_ptr <Singleton>(new Singleton());
});
return *instancePtr_;
}
// 禁止复制与移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
// 示例方法
void doSomething() {
std::lock_guard<std::mutex> lock(mtx_);
std::cout << "Doing something in singleton, thread ID: " << std::this_thread::get_id() << std::endl;
}
private:
Singleton() {
std::cout << "Singleton constructed, thread ID: " << std::this_thread::get_id() << std::endl;
}
~Singleton() = default;
static std::once_flag initFlag_;
static std::unique_ptr <Singleton> instancePtr_;
std::mutex mtx_; // 保护成员数据
};
// 静态成员初始化
std::once_flag Singleton::initFlag_;
std::unique_ptr <Singleton> Singleton::instancePtr_;
3. 关键点说明
-
std::call_once与std::once_flagstd::call_once确保给定的 lambda 在多线程环境下只被执行一次。std::once_flag用于标记是否已执行,内部实现已经做了高效的原子操作和锁。
-
使用
std::unique_ptr- 通过智能指针管理单例生命周期,确保程序退出时自动析构。
- 也避免了裸指针的悬挂指针风险。
-
禁止拷贝与移动
- 单例必须唯一,拷贝/移动构造/赋值会破坏这一约束。
-
线程安全的成员操作
- 对单例内部需要线程保护的成员使用
std::mutex或更细粒度的同步机制。
- 对单例内部需要线程保护的成员使用
-
懒加载
instancePtr_在第一次调用instance()时才会被创建。若不需要使用单例,则不必开辟资源。
4. 使用示例
#include <thread>
void worker() {
Singleton::instance().doSomething();
}
int main() {
std::thread t1(worker);
std::thread t2(worker);
std::thread t3(worker);
t1.join();
t2.join();
t3.join();
return 0;
}
运行结果示例(线程 ID 可能不同):
Singleton constructed, thread ID: 140123456789120
Doing something in singleton, thread ID: 140123456788064
Doing something in singleton, thread ID: 140123456787008
Doing something in singleton, thread ID: 140123456785952
可以看到,单例构造函数仅被调用一次,随后所有线程共享同一个实例。
5. 性能考量
- 首次访问开销:
std::call_once需要一次轻量级锁判断,几乎无开销。 - 后续访问开销:直接返回已创建的对象指针,几乎为零。
- 多线程环境:只有在第一次访问时才会有同步争抢,后续访问不再涉及锁。
6. 进阶:使用 std::atomic 进一步简化
如果单例本身不需要在构造后再初始化其他资源,可以使用 std::atomic<Singleton*> 并配合 std::call_once,避免 std::unique_ptr 的使用。示例:
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag_, []{
instancePtr_.store(new Singleton(), std::memory_order_release);
});
return *instancePtr_.load(std::memory_order_acquire);
}
// ...
private:
static std::once_flag initFlag_;
static std::atomic<Singleton*> instancePtr_;
};
然而,std::unique_ptr 更安全、更易维护,通常推荐使用。
7. 小结
- C++20 的并发特性让实现线程安全、懒加载单例变得简单且高效。
- 关键是
std::call_once+std::once_flag的组合,配合std::unique_ptr,即可获得安全且易用的单例。 - 在实际项目中,可以根据业务需要进一步扩展单例的功能,例如延迟初始化、双重检查锁定等,但不必过度复杂化。
通过上述实现,你可以在任何需要全局唯一对象的场景下安全、简洁地使用 C++20 单例模式。