不能直接用全局 std::shared_ptr 初始化日志对象,因为多线程下全局静态变量初始化不具原子性,c++11 仅保障函数局部 static 变量的线程安全初始化,全局 shared_ptr 构造可能被多个线程重复执行,导致资源泄漏或崩溃。

为什么不能直接用全局 std::shared_ptr<logger></logger> 初始化日志对象
多线程下静态变量的首次初始化不是原子的,哪怕你写了 static std::shared_ptr<logger> g_logger = std::make_shared<logger>();</logger></logger>,在 GCC 或 Clang 的某些优化级别下,仍可能触发多个线程同时执行构造逻辑,导致重复初始化、资源泄漏甚至崩溃。C++11 虽保证了函数局部 static 变量的线程安全初始化,但全局命名空间里的 shared_ptr 不享受该保障。
实操建议:
- 把日志实例封装进函数内部,利用 C++11 的函数级静态初始化语义:
static std::shared_ptr<logger> get_logger() { static auto instance = std::make_shared<logger>(); return instance; }</logger></logger> - 避免在
main()之前(如全局对象构造器里)调用日志——此时线程环境未就绪,std::call_once可能失效 - 若必须提前注册(比如 signal handler 里要打日志),改用
std::atomic<logger></logger>+ 手动 CAS 初始化,但需确保 Logger 构造无锁且不抛异常
如何让 spdlog::logger 在多线程下真正线程安全
spdlog 默认是线程安全的,但它只保证「日志写入」操作线程安全,不保证「配置变更」(如 add_sink、set_level、flush_on)并发执行的安全性。高并发场景下,如果一边有线程在调用 logger->set_level(),另一边正往里写日志,可能触发内部 sink 链表迭代异常或 level 检查错乱。
实操建议:
- 所有配置类操作(
set_level、add_sink、flush_on)严格限制在单线程完成,最好在main()启动后、启动 worker 线程前一次性配好 - 运行时动态调整 level?改用原子变量控制:定义
static std::atomic<:level::level_enum> g_log_level{spdlog::level::info};</:level::level_enum>,在 logger 的should_log()回调里读它,而不是调用logger->level() - 别依赖
spdlog::default_logger_raw()—— 它只是个指针别名,指向的 logger 仍可能被其他模块误改配置
std::call_once 和 std::once_flag 在日志初始化中容易被滥用的点
很多人以为只要套一层 std::call_once 就万事大吉,但实际常见错误是:把 once_flag 放在栈上、或作为类成员却没考虑对象生命周期;更隐蔽的是,在信号处理函数中调用它——std::call_once 内部依赖 mutex,而 signal handler 中调用非 async-signal-safe 函数是未定义行为。
实操建议:
-
std::once_flag必须是静态存储期(global 或 static local),否则 flag 自身可能被析构后再次访问 - 不要在
signal、atexit或__attribute__((constructor))函数里触发日志初始化 - 如果初始化逻辑较重(比如加载配置文件、连接远程 syslog),建议显式分离“首次检查”和“首次创建”,用
std::atomic<bool></bool>先快速判断是否已就绪,避免每次日志都走call_once的 fence 开销
为什么推荐用 std::shared_ptr<:logger></:logger> 而不是裸指针或引用
裸指针无法表达所有权,容易在模块卸载时提前释放 logger;引用则无法重绑定,失去运行时切换 logger 实例的能力。而 shared_ptr 天然支持跨模块共享、延迟销毁,并与 RAII 完全兼容。但要注意:它的线程安全仅限于引用计数本身,不保护所指对象的内部状态。
实操建议:
- 全局 logger 变量声明为
static std::shared_ptr<:logger> g_logger;</:logger>,初始化入口统一收口到一个函数(如init_logging()) - 各模块通过
g_logger->info(...)使用,无需再传参或依赖单例类——降低耦合,也避免模板实例化爆炸 - 如果程序支持热重载模块,确保 logger 实例生命周期长于所有使用者;必要时用
weak_ptr检查有效性,而非捕获空指针异常
最麻烦的从来不是“怎么创建唯一实例”,而是“谁在什么时候销毁它”——尤其当 logger 内部持有线程池、文件句柄或网络连接时,析构顺序、线程等待、信号中断都会让看似简单的全局对象变成悬挂指针温床。动手前先想清楚:这个 logger 是整个进程生命周期的,还是某个子系统的?后者就别塞进全局变量里了。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!








