直接用std::ofstream写日志会卡主线程,因磁盘I/O阻塞,频繁同步写入导致吞吐量骤降;双缓冲+异步线程可解:业务线程写入一缓冲区,日志线程刷另一缓冲区,通过std::atomic交换指针实现无锁切换。

为什么直接用 std::ofstream 写日志会卡主线程
因为磁盘 I/O 是阻塞的,哪怕只是 write() 一个 4KB 缓冲区,内核也可能把线程挂起几十微秒到几毫秒——尤其在高负载或机械盘上。而日志调用往往散落在业务逻辑各处(比如每处理一个请求就 log.info() 一次),频繁同步写入会让吞吐量断崖下跌。
双缓冲 + 异步线程是解法核心:一个缓冲区供业务线程写入(push_log()),另一个由日志线程刷盘(flush_to_file()),两者通过原子指针交换,避免锁竞争。
- 别用
std::mutex保护整个缓冲区——它会变成性能瓶颈 - 别让业务线程等日志线程完成刷盘——必须无等待返回
- 缓冲区大小建议设为
64 * 1024(64KB),太小导致频繁交换,太大增加内存占用和单次刷盘延迟
如何用 std::atomic 实现无锁双缓冲区切换
关键不是“两个数组”,而是两个指针指向当前可写/可刷的缓冲区。用 std::atomic<int></int> 或 std::atomic<bool></bool> 标记状态,但更稳妥的是用 std::atomic<char></char> 直接交换缓冲区地址。
示例片段(简化):
class AsyncLogger {
char buf_a[65536];
char buf_b[65536];
std::atomic<char> current_buf{buf_a};
std::atomic<char> next_buf{buf_b};
std::atomic<size_t> used{0};
<pre class="brush:php;toolbar:false;">void push(const char* msg, size_t len) {
char* b = current_buf.load();
size_t u = used.load();
if (u + len <p>};</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/gongju/2823" title="C++14"><img
src="https://img.php.cn/upload/manual/001/431/639/6ac8b33c327c4749.png" alt="C++14" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/gongju/2823" title="C++14" class="overflowclass">C++14</a>
<p class="overflowclass">C++14 对 C++11 的修正与增强版本,适合旧系统维护和较老工具链兼容。</p>
</div>
<a rel="nofollow" href="/xiazai/gongju/2823" title="C++14" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>
- 交换前必须确保
used已更新,否则新缓冲区可能覆盖未刷出的数据 - 不能用
std::atomic<char></char>—— 数组类型不可原子化 - 交换操作本身要轻量,
exchange()是硬件级原子指令,比compare_exchange_weak更适合这种“强制换”的场景
异步线程如何安全刷盘而不丢日志
日志线程不能简单地“每隔 10ms 刷一次”,必须响应缓冲区满、显式 flush() 请求、以及程序退出信号。否则在崩溃前最后一段日志就丢了。
- 用
std::condition_variable+std::mutex唤醒刷盘线程,但只用于通知“有数据可刷”,不用于保护缓冲区内容 - 刷盘前先调用
fflush(fp),再调用fsync(fileno(fp))(Linux/macOS)或_commit(_fileno(fp))(Windows),否则数据可能卡在 libc 缓冲区或页缓存里 - 记录刷盘偏移量(
off_t)而非缓冲区索引,避免因换行符位置变化导致日志错位 - 如果
fwrite()返回值小于预期长度,说明磁盘已满或权限不足——此时应降级到 stderr 输出错误,并停止写入该文件
实际部署时最容易被忽略的三个细节
很多实现能跑通测试,但上线后出问题,往往栽在这三点:
-
open()文件时漏掉O_CLOEXEC(Linux)或FILE_FLAG_NO_BUFFERING(Windows)——子进程继承句柄后可能干扰日志文件,或绕过系统缓存导致性能反降 - 没处理
SIGPIPE信号:当日志文件被外部rm或logrotate切走,write()会触发该信号,默认终止进程 - 时间戳格式用
std::chrono::system_clock::now()而非clock_gettime(CLOCK_MONOTONIC, ...)——系统时间被 NTP 调整时,日志时间会倒流或跳变
双缓冲的本质是用确定的内存开销换不确定的 I/O 延迟,但它不会自动解决磁盘满、权限错、路径不存在这些底层问题——这些必须在初始化阶段做预检,而不是等到第一条日志写入时才暴露。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










