直接用 std::async 包日志会出问题,因缺乏自动生命周期管理,导致乱序、崩溃或访问已释放内存;必须显式实现输入隔离、执行解耦和输出同步。

为什么直接用 std::async 包日志会出问题
异步日志不是简单把 log() 丢进 std::async 就完事。常见现象是:日志乱序、崩溃在 std::string 析构时、或主线程退出后后台线程还在访问已释放的 std::shared_ptr 缓冲区。
根本原因是 C++ 没有“自动生命周期绑定”的装饰器语义——你不能只写一个包装函数,就指望它自动管理缓冲、线程安全、资源回收。必须显式拆解三件事:输入隔离(拷贝参数)、执行解耦(队列+worker)、输出同步(如文件 append 原子性)。
实操建议:
- 所有传入日志内容必须深拷贝:用
std::string而非const std::string&,避免引用悬空 - 禁用
std::async(std::launch::async, ...)直接调用;改用无锁环形队列(如moodycamel::ConcurrentQueue)+ 单独日志线程 - 日志线程退出前,必须 drain 队列并 flush 文件句柄,否则最后几条日志永远不落地
LogDecorator 类怎么设计才不和业务逻辑耦合
装饰器的核心不是“加功能”,而是“不改变被装饰对象的接口”。如果你让业务代码从调用 logger->info(...) 变成 decorator->info(...),那就不是装饰,是重写。
立即学习“C++免费学习笔记(深入)”;
正确做法是让 LogDecorator 继承同一抽象基类(比如 LoggerInterface),并在构造时接收原始 LoggerInterface*。它只负责前置/后置增强,不接管核心实现。
示例关键结构:
class LoggerInterface {
public:
virtual void info(const std::string& msg) = 0;
virtual void error(const std::string& msg) = 0;
};
<p>class AsyncLogDecorator : public LoggerInterface {
LoggerInterface<em> inner<em>;
moodycamel::ConcurrentQueue<LogEntry> queue</em>;
std::thread worker_;
public:
AsyncLogDecorator(LoggerInterface</em> inner) : inner<em>(inner) {
worker</em> = std::thread([this]{ run<em>worker(); });
}
void info(const std::string& msg) override {
queue</em>.enqueue({Level::INFO, std::string(msg)}); // 深拷贝
}
private:
void run<em>worker() {
LogEntry e;
while (running</em> || !queue_.try<em>dequeue(e)) {
if (queue</em>.try<em>dequeue(e)) inner</em>->info(e.msg); // 交给真实 logger
}
}
};</p>注意:inner_ 是裸指针,但由上层控制生命周期(比如用 std::unique_ptr 管理整个装饰链),装饰器本身不 new/delete。
层级封装时 FileLogger 和 ConsoleLogger 怎么共用异步能力
如果每个具体 logger 都自己实现一套队列+线程,代码重复且状态难统一(比如想全局暂停日志,得遍历所有 worker)。应该把异步能力下沉为可插拔的“执行策略”。
推荐用策略模式配合装饰器:定义 LogExecutionPolicy 接口,提供 submit(std::function<void()>) 方法。默认实现是 async + queue;测试时可换成 immediate 执行(方便断言)。
这样封装后,FileLogger 和 ConsoleLogger 只需专注写文件/写终端,异步调度完全解耦:
-
AsyncLogDecorator不依赖具体 logger 类型,只依赖LoggerInterface和LogExecutionPolicy - 切换策略只需换一个 policy 对象,不用改 logger 或 decorator 的源码
- 性能影响明确:policy 决定是否多线程、是否批处理、是否限流
最容易被忽略的崩溃点:std::localtime 和时区缓存
很多日志装饰器会在格式化时调用 std::localtime(&ts) 获取年月日。这是线程不安全的——localtime 返回静态缓冲区指针,多个日志线程同时调用会覆盖彼此结果。
必须用线程安全替代方案:
- C++20 用
std::chrono::zoned_time+std::format - 旧标准用
std::localtime_r(Linux/glibc)或localtime_s(MSVC) - 更稳妥:预生成时间字符串放队列里,避免在 worker 线程里做格式化(减少锁和系统调用)
另外,std::ofstream 的 << 操作不是原子的,多线程直写同一个文件句柄会导致内容错乱。要么加 mutex,要么让 worker 线程独占文件句柄并保证顺序写入。


















