直接用 std::thread 写日志易卡死或丢日志,因其缺乏队列、同步与生命周期管理;需配合 mutex、condition_variable 和明确关闭协议,由专属 I/O 线程独占操作文件流,主线程仅安全入队。

为什么直接用 std::thread 写日志容易卡死或丢日志
因为 std::thread 本身不提供队列、同步、生命周期管理——你手动 push_back 日志字符串到一个 std::queue,再让线程循环 pop 写文件,稍不留神就会触发数据竞争或析构时访问已销毁的 std::ofstream。常见现象是程序退出时崩溃,或者高并发下日志重复/缺失。
关键不是“启个线程”,而是“安全地把日志从主线程移交到 I/O 线程”。必须配合同步原语和明确的关闭协议。
- 日志缓冲区必须用
std::queue+std::mutex+std::condition_variable组合,不能只靠mutex锁住整个push/pop - 主线程不能等线程自然结束——要发“停止信号”,然后
join(),否则std::thread析构会调用std::terminate - 文件流(
std::ofstream)必须在线程内构造、使用、关闭,不能跨线程传递对象实例
std::async 和 std::thread 哪个更适合日志系统
std::async 不适合。它默认可能延迟启动、可能被调度到线程池、返回的 std::future 如果不 get() 或 wait() 会导致析构阻塞——这和“非阻塞”目标直接冲突。而 std::thread 虽需手动管理,但控制粒度更细,能明确绑定专属 I/O 线程。
真正该做的是:用 std::thread 启一个长期运行的 worker 线程,内部用 wait_for 配合超时避免空转,同时支持快速响应停止指令。
立即学习“C++免费学习笔记(深入)”;
- 别用
std::launch::deferred,它根本没开新线程 - 别在日志函数里反复创建/销毁
std::thread——开销大且不可控 - worker 线程主循环应类似:
while (!stop_requested) { cv.wait_for(...); process_queue(); }
如何保证多线程写同一个日志文件不出错
不是靠“加锁写文件”,而是靠“只让一个线程写”。日志类内部的 std::ofstream 必须是私有成员,且只在 I/O 线程中打开、刷新、关闭。主线程只负责把格式化后的字符串塞进线程安全队列,其余一概不管。
文件名拼接、时间戳生成、滚动判断(比如按天切文件)这些逻辑,也必须放在 I/O 线程里做——否则主线程拿到的时间和实际写入时间可能跨天,导致日志错位。
- 不要在主线程调用
ofs ,那是对 <code>std::ofstream的直接操作,不线程安全 - 滚动策略要用绝对路径+当前日期计算,不要依赖
std::chrono::system_clock::now()在主线程取值 - 每次写入后建议调用
ofs.flush(),尤其在非高频场景下,避免缓冲区滞留
一个可直接编译的最小可行实现(C++11)
下面代码满足:构造即启动线程、支持 log("msg") 非阻塞调用、程序退出前自动 flush 并安全 join。所有平台(Linux/macOS/Windows)可用,无需第三方库。
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <fstream>
#include <string>
#include <chrono>
#include <ctime>
class AsyncLogger {
std::queue<std::string> log_queue_;
std::mutex mtx_;
std::condition_variable cv_;
std::thread worker_;
std::atomic<bool> stop_requested_{false};
std::ofstream ofs_;
void worker_loop() {
while (true) {
std::string msg;
{
std::unique_lock<std::mutex> lk(mtx_);
cv_.wait_for(lk, std::chrono::milliseconds(10),
[this]{ return stop_requested_ || !log_queue_.empty(); });
if (stop_requested_ && log_queue_.empty()) break;
if (!log_queue_.empty()) {
msg = std::move(log_queue_.front());
log_queue_.pop();
}
}
if (!msg.empty()) {
if (!ofs_.is_open()) {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << "log_" << std::put_time(std::localtime(&time_t), "%Y%m%d") << ".txt";
ofs_.open(ss.str(), std::ios::app);
}
if (ofs_.is_open()) {
ofs_ << msg << "\n";
ofs_.flush();
}
}
}
if (ofs_.is_open()) ofs_.close();
}
public:
AsyncLogger() : worker_(&AsyncLogger::worker_loop, this) {}
~AsyncLogger() {
stop_requested_ = true;
cv_.notify_one();
if (worker_.joinable()) worker_.join();
}
void log(const std::string& msg) {
std::lock_guard<std::mutex> lk(mtx_);
log_queue_.push(msg);
cv_.notify_one();
}
};
注意:这里没做异常捕获(如磁盘满导致 ofs_<< 失败),也没处理日志长度限制——真实项目中,这两点一旦忽略,轻则静默丢日志,重则线程卡死在 operator<< 内部。别只抄结构,得根据部署环境补上错误分支。



















