在 C++17 之后,使用函数内的静态变量已经天然线程安全。C++20 进一步加强了初始化保证,使得单例实现可以更简洁、更可靠。下面给出一种典型实现,并讨论其线程安全性与延迟初始化。
// Singleton.h
#pragma once
#include <mutex>
class Singleton {
public:
// 取得全局唯一实例
static Singleton& instance() noexcept {
// 这里的 static 对象在第一次进入此函数时进行初始化
// 并且 C++20 保证此初始化是线程安全的
static Singleton instance;
return instance;
}
// 公开的业务方法
void do_something() {
std::lock_guard<std::mutex> lock(mtx_);
// 这里执行需要线程同步的业务
// 例如更新内部状态、写日志等
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
// 私有构造函数,防止外部实例化
Singleton() = default;
~Singleton() = default;
std::mutex mtx_; // 保护内部状态的互斥量
};
关键点解析
-
静态局部变量
static Singleton instance;在第一次调用instance()时被构造。自 C++11 起,编译器会为其加上锁,确保多线程同时进入时仅有一次构造。C++20 进一步保证构造过程是异常安全的:如果构造抛异常,后续调用会重新尝试初始化。 -
noexcept修饰
instance()用noexcept声明,说明此函数不会抛异常,便于编译器优化。 -
内部互斥
虽然实例化本身是线程安全的,但如果单例内部有可变状态(如缓存、计数器等),仍需使用互斥量或原子操作保护。 -
禁止拷贝与赋值
通过删除拷贝构造和赋值运算符,确保只能有一个唯一实例。
延迟初始化 vs 立即初始化
- 延迟初始化(如上所示):仅在第一次真正需要单例时才构造,节省启动资源。
- 立即初始化(静态全局对象):在程序启动时即构造,适用于必须在任何线程之前完成初始化的场景。
何时需要手动加锁?
如果单例内部的业务方法需要频繁修改共享状态,而不想在每个调用中加锁,可考虑使用 std::atomic 或读写锁 std::shared_mutex,根据读写比例优化性能。
完整示例
#include "Singleton.h"
#include <iostream>
#include <thread>
void worker() {
Singleton& s = Singleton::instance();
s.do_something();
std::cout << "Thread " << std::this_thread::get_id() << " finished\n";
}
int main() {
std::thread t1(worker);
std::thread t2(worker);
t1.join();
t2.join();
return 0;
}
运行上述程序时,单例实例只会被构造一次,且多线程间的调用是安全的。通过 std::lock_guard 或者更高级的同步原语,可以进一步保证业务逻辑的线程安全。
这样,一个在 C++20 标准下既简洁又安全的单例实现就完成了。