在开发高并发系统时,日志是排查问题的重要工具。但你有没有遇到过这种情况:多个线程同时写日志,结果日志内容混在一起,一行还没写完,另一行的内容就插了进来?这种日志错乱让调试变得异常困难。
为什么普通日志打印不安全
假设你用一个简单的文件写入方式记录日志:
void log(const char* msg) {
FILE* f = fopen("app.log", "a");
fprintf(f, "[%ld] %s\n", time(NULL), msg);
fclose(f);
}
这段代码在单线程下没问题,但在多线程环境中,两个线程可能同时打开同一个文件,写入位置重叠,导致日志交错甚至丢失。
加锁是最直接的解决办法
给日志函数加上互斥锁,就能保证同一时间只有一个线程能写日志:
#include <pthread.h>
pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER;
void safe_log(const char* msg) {
pthread_mutex_lock(&log_mutex);
FILE* f = fopen("app.log", "a");
fprintf(f, "[%ld] %s\n", time(NULL), msg);
fclose(f);
pthread_mutex_unlock(&log_mutex);
}
这样一来,即使十个线程同时调用 safe_log,也会排队执行,日志顺序清晰可读。
异步日志提升性能
虽然加锁解决了安全问题,但频繁写磁盘会影响性能。更高效的做法是把日志写入一个线程安全的队列,由单独的日志线程负责写文件。
比如用一个循环缓冲区配合原子操作或互斥锁保护,主线程只做快速入队,后台线程慢慢处理落盘。这样既保证了线程安全,又减少了对业务线程的阻塞。
使用成熟的日志库
自己实现容易出错,大多数项目会直接选用像 spdlog、glog 或 log4cpp 这类支持线程安全的日志库。它们默认做了锁保护,有些还支持异步模式,配置简单,稳定性高。
比如 spdlog 的基本用法:
#include <spdlog/spdlog.h>
int main() {
auto logger = spdlog::basic_logger_mt("file_logger", "logs/basic.txt");
logger->info("这是一个线程安全的日志消息");
return 0;
}
其中 basic_logger_mt 的 mt 就表示 multi-threaded,内部已做好同步处理。
实际场景中的坑
有位开发者在做域名解析服务时,给每个请求都打日志。系统上线后并发一上来,日志文件里出现了大量半截内容,像是“[1712345678] 正在解析www.xxx”后面突然接上了另一个域名。查了半天才发现是日志没做同步。后来改用 spdlog 异步模式,问题立刻消失,服务也更稳了。
线程安全的日志不是可有可无的优化,而是高并发服务的基础设施。别等到线上出问题才回头补课。