不能直接用 std::async 或单日志线程,因其无法跨平台可靠设置OS调度优先级,且无法解决高优日志(如ERROR)被低优日志阻塞的核心矛盾;应采用带优先级队列的线程池,将日志作为带level和timestamp的任务单元入队,工作线程从std::priority_queue top()取任务,level低者优先、相同时按时间防饥饿。

为什么不能直接用 std::async 或单个日志线程
因为优先级调度在标准 C++ 线程库中不暴露给用户:std::async 启动的线程、std::thread 创建的线程,其 OS 级调度优先级无法跨平台可靠设置;Linux 的 pthread_setschedparam 和 Windows 的 SetThreadPriority 都需要额外权限或策略支持,且容易被线程池内部复用逻辑覆盖。更关键的是,异步日志的核心矛盾不是“并发写”,而是“高优先级日志(如 ERROR)不能被低优先级日志(如 DEBUG)阻塞”。单线程串行化会卡住高优任务,多线程无序竞争又可能让 ERROR 日志晚于 100 条 INFO 才落盘。
用带优先级队列的线程池替代“日志线程”
核心是把“日志消息”变成带优先级的任务单元,由统一的线程池按优先级分发执行。不要自己管理线程生命周期,复用成熟的线程池(如 boost::asio::thread_pool 或自研无锁队列 + 工作窃取),但必须替换其默认的 FIFO 任务队列为基于 std::priority_queue 的变体。
- 定义日志任务结构体,含
level(int,ERROR=0, WARN=1, INFO=2, DEBUG=3)、timestamp、message、file/line等字段 - 重载
operator:优先比较 <code>level,相同时用timestamp防止饥饿(避免同 level 下新日志永远排在老日志后面) - 线程池工作线程从优先级队列
top()取任务,而非front();每次取完调用pop() - 注意:C++ 标准
std::priority_queue是最大堆,所以要让低数值 level 表示高优先级,即return a.level > b.level;
如何避免日志内容拷贝和内存抖动
高频日志场景下,std::string 拷贝和临时对象构造是主要性能瓶颈。不能在每条日志都做完整格式化(如拼接时间、文件名、级别字符串)后再入队。
- 入队前只保存原始参数:用
std::variant<:string_view int double const char></:string_view>存储未格式化的值,延迟到工作线程中再格式化 - 使用
fmt::format_to配合预分配的fmt::memory_buffer,避免反复 new/delete - 对固定字段(如模块名、函数名)用
static constexpr std::string_view,确保编译期驻留,零拷贝 - 慎用
std::shared_ptr<logentry></logentry>:引用计数有原子开销;改用std::unique_ptr<logentry></logentry>+ 移动语义,或更轻量的 arena 分配器
flush 控制与线程安全边界在哪
日志系统最易出错的不是写入,而是 flush 时机和跨线程可见性。比如 ERROR 日志要求“立即刷盘”,但 std::ofstream::flush() 是阻塞的,不能在高优任务中同步调用。
立即学习“C++免费学习笔记(深入)”;
- 将 flush 行为也作为任务提交:例如
log_error("xxx"); log_flush();实际生成两个优先级相同的任务,后者触发fsync() - 避免在日志任务中直接调用
std::cout或全局std::ofstream—— 多线程写同一文件流需加锁,锁粒度大会拖慢整个池;应每个工作线程持有一个独立std::ofstream(复用句柄,通过seekp(0, std::ios::end)定位),或用无锁 ring buffer + 单独 I/O 线程消费 -
std::atomic<bool></bool>标记是否启用日志,但修改它不保证已入队任务被丢弃;真正开关应作用于提交入口(如检查if (level >= g_min_log_level) return;)
优先级日志最难的不是排序,而是让高优任务“真正更快到达磁盘”——这取决于你是否把 fsync、磁盘 I/O、甚至 page cache 回写都纳入了优先级链路。多数人只做了内存队列排序,却忘了 write() 系统调用本身没有优先级。



















