在现代 C++(C++11 及以后)中,线程安全的单例实现变得相当简单。下面给出一种推荐的实现方式,并解释其内部机制。
1. 使用 std::call_once 与 std::once_flag
#include <mutex>
#include <memory>
class Singleton
{
public:
// 访问单例的全局接口
static Singleton& instance()
{
// 1. static 局部变量在第一次进入函数时初始化
// C++11 规定此初始化是线程安全的
static Singleton* instance_ptr = nullptr;
static std::once_flag init_flag;
// 2. std::call_once 确保 lambda 只执行一次
std::call_once(init_flag, []{
instance_ptr = new Singleton();
});
return *instance_ptr;
}
// 禁止复制和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
// 示例成员函数
void do_something() const
{
// ...
}
private:
Singleton() { /* 复杂初始化代码 */ }
~Singleton() { /* 清理资源 */ }
};
关键点说明
-
*`static Singleton instance_ptr
** 使用裸指针而非std::unique_ptr或std::shared_ptr`,因为在程序退出时我们不需要析构单例。裸指针更轻量,并避免在退出时产生析构顺序问题。 -
std::once_flag+std::call_once
std::once_flag确保后续的std::call_once调用只会执行一次。即使多个线程同时调用instance(),也只有一个线程会执行 lambda 初始化,其他线程会阻塞等待直到初始化完成。 -
删除复制/移动构造
防止外部错误地复制或移动单例,保证唯一性。
2. 简化版本:直接使用 static 局部对象
如果你不介意在程序退出时自动销毁单例,甚至不想手动管理内存,可以直接使用:
class Singleton
{
public:
static Singleton& instance()
{
static Singleton instance; // C++11 线程安全初始化
return instance;
}
// ...
};
这种写法最简洁,但缺点是如果单例在 main 退出前仍然被引用,析构时可能会访问已被销毁的全局对象,导致“静态析构顺序”问题。
3. 与 C++20 std::atomic_flag 的组合
C++20 之后,你还可以使用 std::atomic_flag 替代 std::once_flag,实现更细粒度的控制:
static std::atomic_flag init_flag = ATOMIC_FLAG_INIT;
static Singleton* instance_ptr = nullptr;
if (!init_flag.test_and_set(std::memory_order_acquire)) {
// 第一线程进入此分支,进行初始化
instance_ptr = new Singleton();
init_flag.clear(std::memory_order_release);
}
但相比 std::call_once 代码更繁琐,且易错,通常不推荐。
4. 性能注意
- 第一次调用成本高:需要原子操作和可能的锁,初始化完成后后续调用几乎无开销。
- 多线程竞争:如果多线程频繁访问
instance(),使用std::call_once的开销在第一次访问后几乎为零,随后几乎是纯读操作。
5. 常见陷阱
- 忘记
delete单例:如果你使用裸指针并且不在程序结束前delete它,可能导致内存泄漏。但由于单例的生命周期与程序相同,通常可以忽略释放。 - 析构顺序:若单例内部持有其他全局对象,销毁顺序可能导致访问已销毁对象。使用 “创建时即使用” 或 “惰性删除” 可以规避。
- 静态初始化顺序:如果某个模块在
main之前就需要访问单例,确保单例的instance()调用在该模块的静态构造中不会导致提前实例化。
6. 结论
- 推荐实现:使用
std::call_once+std::once_flag的第一种模式。它既符合现代 C++ 标准,又能保证线程安全且易于维护。 - 简化实现:若不介意析构顺序问题,直接使用
static局部对象即可。 - 性能优化:对性能极限的需求,建议使用
std::once_flag并在单例中加入锁粒度控制。
通过上述方法,你可以在任何 C++11+ 项目中安全、简洁地实现线程安全的单例。