直接用fopen+fwrite写日志出错,因file*非线程安全,多线程并发操作缓冲区和文件偏移导致错乱;即使posix open+write加o_append,字符串拼接仍不安全;推荐用std::mutex全局保护“格式化+写入”全过程。

为什么直接用 fopen + fwrite 写日志会出错
多个线程同时调用 fopen("log.txt", "a") 打开同一文件,再各自 fwrite,大概率导致日志内容错乱、覆盖或截断。根本原因不是“文件没锁”,而是:FILE* 对象本身不是线程安全的——fprintf、fwrite 等函数内部会操作缓冲区和文件偏移,多线程并发调用时,缓冲区状态和系统调用顺序无法保证。
- 即使只用
open()+write()(POSIX),若未加O_APPEND标志,多个线程lseek+write仍会相互覆盖 - 加了
O_APPEND后,单次write()是原子的(只要不超过PIPE_BUF,通常是 4KB),但日志通常含时间戳+线程ID+消息,拼接字符串过程本身就不安全 - 频繁打开/关闭文件(每条日志都
fopen/fclose)性能极差,且可能触发系统文件描述符耗尽
用 std::mutex 包裹日志写入是最简单可靠的方案
在 C++11 及以上,全局一个 std::mutex 保护所有日志写入逻辑,简单、可读、不易出错。关键点在于:锁的粒度要包住「格式化 + 写入」整个过程,不能只锁 fwrite。
class Logger {
std::mutex mtx_;
FILE* file_ = nullptr;
public:
Logger(const char* path) {
file_ = fopen(path, "a");
if (!file_) throw std::runtime_error("cannot open log file");
}
void log(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
std::lock_guard<:mutex> lock(mtx_);
// 格式化到栈缓冲区(避免 new 或静态缓冲区竞争)
char buf[1024];
int n = vsnprintf(buf, sizeof(buf), fmt, args);
if (n > 0 && n <ul>
<li>必须调用 <code>fflush(file_)</code>,否则 <code>FILE*</code> 缓冲区内容可能滞留内存</li>
<li>不要用 <code>static char buf[...]</code>,否则多线程重入时会覆盖;用栈分配或 <code>std::vector<char></char></code> 动态扩容</li>
<li>如果日志量极大(如每秒万级),单 mutex 会成为瓶颈,此时才需考虑无锁队列 + 单独日志线程</li>
</ul>
<h3>用 <code>std::ofstream</code> 配合 <code>std::scoped_lock</code> 更现代但要注意缓冲</h3>
<p><code>std::ofstream</code> 默认不线程安全,且默认开启缓冲,不显式 <code>flush()</code> 容易丢日志。它比 <code>FILE*</code> 更易误用,因为看起来“更 C++”。</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill4025" title="C++ 算法竞赛自动化测试数据生成与校验框架"><img
src="https://img.php.cn/upload/skill/000/000/081/178988956499722.jpg" alt="C++ 算法竞赛自动化测试数据生成与校验框架" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill4025" title="C++ 算法竞赛自动化测试数据生成与校验框架" class="overflowclass">C++ 算法竞赛自动化测试数据生成与校验框架</a>
<p class="overflowclass">根据原题生成新题面、验证器及完整测试数据,自动套用 testlib 模板,用于用户要求生成测试数据时。</p>
</div>
<a rel="nofollow" href="/xiazai/skill4025" title="C++ 算法竞赛自动化测试数据生成与校验框架" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>
<ul>
<li>务必在每次写入后调用 <code>os_ 或 <code>os_.flush()</code>;<code>std::endl</code> 自带 flush,但性能略差</code>
</li>
<li>避免使用 <code>operator 连续写入多个部分(如 <code>os_ ),中间被其他线程打断会导致日志碎片化</code></code>
</li>
<li>推荐先拼好整行字符串(<code>std::string</code>),再一次性写入:<code>os_ </code>
</li>
<li>构造 <code>std::ofstream</code> 时传 <code>std::ios::app</code>,但注意:C++ 标准不保证 <code>app</code> 模式下多线程写入的原子性,仍需 mutex 保护</li>
</ul>
<h3>高吞吐场景下别硬扛,用无锁队列 + 日志线程</h3>
<p>当单 mutex 成为瓶颈(例如压测中 log 耗时占整体 15%+),应把日志生产与消费解耦。核心是:业务线程只负责快速入队,由专用线程串行落盘。</p>
<ul>
<li>用 <code>moodycamel::ConcurrentQueue</code>(轻量、无锁、头文件即用)或 <code>boost::lockfree::queue</code>,避免自己实现有 bug</li>
<li>队列元素建议是 <code>std::string</code>(移动语义高效),而非裸指针或自定义结构体(增加内存管理复杂度)</li>
<li>日志线程用 <code>while(running) { if (queue.try_dequeue(item)) write_to_file(item); }</code>,不要 busy-wait,加 <code>std::this_thread::sleep_for(1ms)</code> 或用条件变量</li>
<li>注意程序退出时需等待队列清空,否则最后几条日志丢失——设置 <code>running = false</code> 后,循环 drain 直到队列为空</li>
</ul>
<p>真正难的不是选哪种方案,而是意识到:日志不是“随便写写”的事。从第一行 <code>fopen</code> 开始,就要决定要不要 flush、要不要锁、要不要分离 IO。漏掉其中一环,线上出问题时你翻日志都找不到线索。</p></:mutex>C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










