日志丢失主因是缓冲区未及时刷新而非线程竞争;std::cout行缓冲(终端)与文件流全缓冲特性不同,异常退出时缓冲数据易丢失,故应避免直接用std::cout写日志。

日志丢失通常不是线程安全问题,而是缓冲区刷新机制失效
在 std::cout 或 std::ofstream 直接写入多线程场景下,日志“消失”大概率不是因为输出被覆盖或竞争写坏,而是因为缓冲区未及时刷新——尤其当程序异常退出、线程提前终止或日志量小导致行缓存未触发刷盘。C++ 标准流默认对 std::cout 启用行缓冲(连接到终端时),但对文件流通常为全缓冲,且无跨线程同步保证。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 避免直接用
std::cout :虽然 <code>std::endl强制刷新,但频繁调用会严重拖慢性能,且无法解决多线程下缓冲区竞争(如多个线程同时 flush) - 改用带显式锁的日志函数封装,内部统一使用
std::flush而非std::endl - 对文件流,构造时显式禁用缓冲:
log_file.rdbuf()->pubsetbuf(nullptr, 0);(注意:部分平台不支持无缓冲文件 I/O,需实测)
std::ofstream 多线程写入必须加锁,且不能依赖流对象自身线程安全
std::ofstream 对象本身不是线程安全的——C++11 标准明确指出,对同一 std::basic_ostream 实例的并发操作(包括 operator 和 <code>flush)是未定义行为。即使每个线程创建独立 std::ofstream 写同一文件,也会因底层系统调用(如 write(2))无原子性导致内容错乱或丢失。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 所有日志写入必须经由一个全局互斥量保护,例如
std::mutex g_log_mutex;,且 lock 范围要覆盖整个格式化 + 写入 + flush 过程 - 不要用多个
std::ofstream对象轮流写同一个文件;应只持有一个打开的流对象,由锁控制访问 - 避免在锁内做耗时操作(如字符串拼接、
std::chrono::system_clock::now()),可先在锁外准备好完整日志字符串
用 std::atomic 控制日志开关时,记得用 memory_order_seq_cst
调试高并发日志时,常通过全局原子变量(如 std::atomic<bool> g_enable_debug_log</bool>)动态开启/关闭日志。若使用弱内存序(如 memory_order_relaxed),可能因编译器重排或 CPU 乱序,导致某个线程看到开关已关闭,却仍执行了旧的、本该跳过的日志分支——看似“丢失”,实则是条件判断失效。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 读写该标志一律用默认(即
memory_order_seq_cst),无需优化:日志开关本身不是性能瓶颈 - 检查是否在 if 分支里漏掉了锁 —— 即使开关为 true,也必须进锁后才真正写日志,否则仍存在竞态
- 避免把日志宏展开成带条件的多语句,例如
#define LOG(x) if(g_enable...) { lock(); ,宏展开易出错,推荐用内联函数
用 strace 或 ltrace 验证 write 系统调用是否真实发出
当怀疑日志“丢失”但代码逻辑看似正确时,最直接的办法是绕过 C++ 流层,确认内核是否真的收到了写请求。用 strace -e write,writev -p <pid></pid> 可捕获进程所有 write 类系统调用;若没看到对应日志内容的 write,说明问题在用户态缓冲或条件分支;若看到了但文件里没有,可能是文件系统缓存(sync 未触发)或程序崩溃前未返回。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 测试时用
std::this_thread::sleep_for(10ms)拖慢节奏,方便 strace 捕获单条日志 - 对文件流,可在每次 flush 后加
fsync(log_fd)(需用fileno(log_file.rdbuf())获取 fd),验证是否卡在 page cache - 注意:glibc 的
std::ofstream在 close 前可能延迟分配文件描述符,strace 中 write 可能出现在首次写之后,而非构造时


















