在 C++11 之后,标准库为多线程编程提供了诸多工具,其中最常用的是 std::call_once 和 std::once_flag。
利用它们可以轻松实现 线程安全的单例,而不必担心竞争条件或双重检查锁定(Double‑Checked Locking)带来的陷阱。
下面给出一个完整示例,说明如何:
- 使用
std::call_once和std::once_flag延迟初始化单例。 - 在 C++17/20 时代通过
inline静态成员或constexpr构造函数进一步简化。 - 讨论懒加载(Lazy Loading)与饿汉模式(Eager Initialization)的权衡。
1. 基础实现
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
class Singleton {
public:
// 公开获取实例的静态成员函数
static Singleton& instance() {
std::call_once(initFlag_, []() {
instance_.reset(new Singleton());
});
return *instance_;
}
// 演示用的成员函数
void do_something() const {
std::cout << "Singleton instance address: " << this << std::endl;
}
// 禁止拷贝和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() { std::cout << "Singleton constructed\n"; }
~Singleton() = default;
static std::unique_ptr <Singleton> instance_;
static std::once_flag initFlag_;
};
// 静态成员定义
std::unique_ptr <Singleton> Singleton::instance_;
std::once_flag Singleton::initFlag_;
关键点
std::call_once保证传入的 lambda 只会被执行一次,即使在多线程环境下。std::once_flag是线程安全的同步原语,避免了传统的互斥锁。- 通过
unique_ptr管理实例,避免手动delete。 - 删除拷贝构造函数和赋值运算符,防止复制单例。
2. C++17 版本:inline 静态成员
C++17 引入了 inline 静态成员变量,允许在类内部直接初始化。这样可以进一步简化代码:
class Singleton {
public:
static Singleton& instance() {
// 这里不需要 std::call_once,因为 C++17 的局部静态变量是线程安全的
static Singleton instance; // 线程安全且惰性初始化
return instance;
}
void do_something() const {
std::cout << "Singleton instance address: " << this << std::endl;
}
private:
Singleton() { std::cout << "Singleton constructed\n"; }
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
优点:代码更简洁、无外部同步变量。
缺点:在非常旧的编译器或未支持 C++17 的环境下不可用。
3. C++20 版本:std::atomic 与 std::optional
C++20 为 std::optional 提供了原子化访问,可以写出更现代的单例:
#include <optional>
#include <atomic>
class Singleton {
public:
static Singleton& instance() {
static std::optional <Singleton> opt;
static std::atomic <bool> initialized{false};
if (!initialized.load(std::memory_order_acquire)) {
std::call_once(initFlag_, []() {
opt.emplace();
initialized.store(true, std::memory_order_release);
});
}
return *opt;
}
private:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::once_flag initFlag_;
};
std::once_flag Singleton::initFlag_;
4. 饿汉与懒汉的比较
| 方案 | 初始化时机 | 线程安全性 | 资源占用 | 典型用途 |
|---|---|---|---|---|
| 饿汉(静态全局对象) | 程序启动时 | 取决于编译器,通常安全 | 立即分配 | 对象不需要延迟,且初始化简单 |
懒汉(std::call_once 或局部静态) |
第一次使用时 | 自动保证线程安全 | 只在需要时分配 | 需要延迟或昂贵的初始化 |
5. 常见误区
-
错误的双重检查锁定(Double‑Check Locking)
if (!ptr) { std::lock_guard<std::mutex> lock(mtx); if (!ptr) ptr = new Singleton(); // 依赖于内存屏障 }这在 C++ 之前的编译器中不可行;现在推荐直接使用
std::call_once或局部静态变量。 -
使用
new而不释放
单例往往是应用程序生命周期内存在的,但如果你在多线程环境中手动new并在程序结束时忘记delete,可能导致资源泄漏。建议使用智能指针或局部静态。 -
忽视构造函数抛异常
如果单例的构造函数抛异常,std::call_once会把异常重新抛给调用者;随后再次调用instance()会重新尝试初始化。
6. 小结
std::call_once+std::once_flag是最通用且安全的实现方式,兼容 C++11 及以后版本。- 对于 C++17/20,可以直接使用 局部静态变量 或
std::optional+std::atomic,代码更简洁。 - 在多线程环境下,永远不要手写锁来实现单例,除非你充分理解内存模型。
- 了解 饿汉 与 懒汉 的优缺点,选择最适合你项目需求的实现方式。
希望这篇文章能帮助你在 C++ 现代代码中安全、简洁地实现单例模式。祝编码愉快!