在现代 C++ 开发中,单例模式(Singleton)经常用于需要全局唯一实例的场景,例如日志系统、配置管理器或连接池。若单例不保证线程安全,可能导致多线程环境下的竞争条件、数据损坏或程序崩溃。下面我们以 C++17 为例,分别讨论两种常见的线程安全懒汉式实现:std::call_once + std::once_flag(推荐)和“双检锁(Double-Check Locking)”,并给出完整代码示例与关键点说明。
1. std::call_once + std::once_flag(推荐方式)
1.1 方案思路
C++11 引入了 std::once_flag 与 std::call_once,可在多线程环境中保证函数只被执行一次。单例对象的创建可以放在 std::call_once 回调中,从而避免锁竞争与多次初始化。
1.2 代码实现
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
class Logger {
public:
static Logger& instance() {
std::call_once(initFlag_, []() {
// 用 make_unique 更安全,C++14 起可直接使用
instance_ = std::make_unique <Logger>();
});
return *instance_;
}
void log(const std::string& msg) {
std::lock_guard<std::mutex> lk(ioMutex_);
std::cout << "[LOG] " << msg << std::endl;
}
private:
Logger() { std::cout << "Logger constructed\n"; }
~Logger() { std::cout << "Logger destroyed\n"; }
// 禁止拷贝和移动
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
static std::unique_ptr <Logger> instance_;
static std::once_flag initFlag_;
std::mutex ioMutex_; // 用于同步打印
};
std::unique_ptr <Logger> Logger::instance_;
std::once_flag Logger::initFlag_;
void worker(int id) {
Logger::instance().log("Thread " + std::to_string(id) + " started");
// 模拟工作
std::this_thread::sleep_for(std::chrono::milliseconds(100));
Logger::instance().log("Thread " + std::to_string(id) + " finished");
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back(worker, i);
}
for (auto& t : threads) t.join();
return 0;
}
1.3 关键点说明
| 关键点 | 说明 |
|---|---|
std::once_flag |
只读数据结构,标识是否已初始化 |
std::call_once |
第一次调用时执行 lambda,后续调用不再执行 |
std::unique_ptr |
防止单例被复制或销毁,自动管理生命周期 |
std::lock_guard |
线程安全的 I/O 操作 |
std::make_unique |
安全、简洁的对象创建 |
2. 双检锁(Double-Check Locking)
2.1 方案思路
双检锁先检查实例是否已创建,若未创建则进入互斥锁再做检查,最后实例化。它在 C++11 的原子类型 std::atomic 支持后成为可行方案。注意避免出现“对象已构造但未对内存屏障可见”的问题,必须使用 std::atomic<Logger*> 并配合 std::memory_order_acquire/release。
2.2 代码实现
#include <iostream>
#include <memory>
#include <atomic>
#include <mutex>
#include <thread>
class Logger {
public:
static Logger& instance() {
Logger* tmp = instance_.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lk(initMutex_);
tmp = instance_.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Logger();
instance_.store(tmp, std::memory_order_release);
}
}
return *tmp;
}
void log(const std::string& msg) {
std::lock_guard<std::mutex> lk(ioMutex_);
std::cout << "[LOG] " << msg << std::endl;
}
private:
Logger() { std::cout << "Logger constructed\n"; }
~Logger() { std::cout << "Logger destroyed\n"; }
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
static std::atomic<Logger*> instance_;
static std::mutex initMutex_;
std::mutex ioMutex_;
};
std::atomic<Logger*> Logger::instance_{nullptr};
std::mutex Logger::initMutex_;
void worker(int id) {
Logger::instance().log("Thread " + std::to_string(id) + " started");
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Logger::instance().log("Thread " + std::to_string(id) + " finished");
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) threads.emplace_back(worker, i);
for (auto& t : threads) t.join();
return 0;
}
2.3 关键点说明
| 关键点 | 说明 |
|---|---|
std::atomic<Logger*> |
原子指针,避免多线程竞争 |
memory_order_acquire/release |
确保构造完成后对所有线程可见 |
std::lock_guard<std::mutex> |
保护初始化区块 |
nullptr 初始化 |
采用原子 nullptr 作为未初始化标志 |
3. 对比与最佳实践
| 方案 | 优点 | 缺点 | 推荐情况 |
|---|---|---|---|
call_once |
简单、安全、无显式锁竞争 | 仅在 C++11 之后可用 | 推荐 |
| 双检锁 | 可以手动控制锁粒度 | 代码更复杂,易错 | 若需自定义内存管理、对象生命周期时可用 |
- 如果项目已使用 C++11/14/17,首选
std::call_once。 - 如果想手动控制实例销毁顺序,可考虑使用
std::shared_ptr或std::unique_ptr与std::call_once的组合。
4. 进一步扩展
-
延迟销毁
单例在程序退出时不一定立即销毁。可以使用std::atexit注册销毁函数,或让std::unique_ptr的析构在main结束时自动调用。 -
多线程初始化性能
对于极高并发初始化,std::call_once依旧是最优选择。其实现通常使用std::once_flag与内部mutex,在多线程环境下开销极小。 -
线程安全的懒加载与惰性求值
C++17 的std::optional与std::shared_future可以与单例结合,进一步提升灵活性。
5. 小结
线程安全的单例在 C++ 开发中至关重要。通过 std::call_once 与 std::once_flag 的组合,既能保证一次性初始化,又能避免显式锁竞争;双检锁则为更细粒度控制提供了可能,但实现更复杂。熟练掌握这两种技术,能够在多线程项目中稳健地使用单例,提升代码质量与运行时安全。