在C++项目开发中,日志是排查问题、记录运行状态的重要工具,传统的同步写日志方式会在写入磁盘时阻塞当前线程,当日志量较大时严重影响程序性能。利用C++11提供的thread库可以很方便地实现异步非阻塞的文件日志系统,让日志写入操作在后台线程执行,主线程无需等待写入完成。

核心设计思路
异步非阻塞日志系统的核心是将日志生产者和消费者解耦,主线程作为生产者生成日志内容,后台工作线程作为消费者负责将日志写入文件,两者通过线程安全的队列通信。整体设计包含以下几个部分:
- 线程安全的日志队列:用于存储主线程提交的日志内容,支持多线程并发写入和读取
- 后台工作线程:由C++11的thread创建,循环从日志队列中取出日志并写入文件
- 日志写入模块:负责打开、写入、关闭日志文件,处理文件滚动等逻辑
- 对外接口:提供简单的日志输出接口,供主线程调用提交日志
线程安全日志队列实现
日志队列需要保证多线程操作时的安全性,这里使用互斥锁和条件变量实现,当队列为空时工作线程阻塞等待,当主线程提交日志后唤醒工作线程。
#include <queue>
#include <mutex>
#include <condition_variable>
#include <string>
class LogQueue {
private:
std::queue<std::string> queue_;
std::mutex mutex_;
std::condition_variable cond_;
public:
// 提交日志到队列
void push(const std::string& log) {
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(log);
cond_.notify_one(); // 唤醒等待的工作线程
}
// 从队列取出日志,队列为空时阻塞
std::string pop() {
std::unique_lock<std::mutex> lock(mutex_);
// 队列为空时等待条件变量
cond_.wait(lock, [this]() { return !queue_.empty(); });
std::string log = queue_.front();
queue_.pop();
return log;
}
// 判断队列是否为空
bool empty() {
std::lock_guard<std::mutex> lock(mutex_);
return queue_.empty();
}
};
后台工作线程与日志写入实现
工作线程启动后循环从日志队列取日志,然后写入到文件中,同时需要处理日志文件的创建、切换和关闭逻辑。
#include <thread>
#include <fstream>
#include <chrono>
#include <ctime>
#include <iostream>
class AsyncFileLogger {
private:
LogQueue log_queue_;
std::thread worker_thread_;
std::ofstream log_file_;
std::string log_file_path_;
bool running_;
// 日志文件最大大小,这里设置为10MB
const size_t MAX_FILE_SIZE = 10 * 1024 * 1024;
// 工作线程执行函数
void worker_func() {
while (running_) {
std::string log_content = log_queue_.pop();
// 检查日志文件是否需要切换
if (!log_file_.is_open() || get_file_size() > MAX_FILE_SIZE) {
rotate_log_file();
}
// 写入日志内容,添加时间戳
auto now = std::chrono::system_clock::now();
time_t now_time = std::chrono::system_clock::to_time_t(now);
struct tm* local_tm = localtime(&now_time);
char time_buf[64];
strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", local_tm);
log_file_ << "[" << time_buf << "] " << log_content << std::endl;
log_file_.flush(); // 立即刷新到磁盘
}
// 退出前把队列中剩余的日志写完
while (!log_queue_.empty()) {
std::string log_content = log_queue_.pop();
if (log_file_.is_open()) {
auto now = std::chrono::system_clock::now();
time_t now_time = std::chrono::system_clock::to_time_t(now);
struct tm* local_tm = localtime(&now_time);
char time_buf[64];
strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", local_tm);
log_file_ << "[" << time_buf << "] " << log_content << std::endl;
}
}
if (log_file_.is_open()) {
log_file_.close();
}
}
// 获取当前日志文件大小
size_t get_file_size() {
if (!log_file_.is_open()) return 0;
log_file_.seekp(0, std::ios::end);
size_t size = log_file_.tellp();
log_file_.seekp(0, std::ios::beg);
return size;
}
// 切换日志文件,按时间生成新文件名
void rotate_log_file() {
if (log_file_.is_open()) {
log_file_.close();
}
auto now = std::chrono::system_clock::now();
time_t now_time = std::chrono::system_clock::to_time_t(now);
struct tm* local_tm = localtime(&now_time);
char time_buf[64];
strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", local_tm);
log_file_path_ = "app_log_" + std::string(time_buf) + ".log";
log_file_.open(log_file_path_, std::ios::app);
if (!log_file_.is_open()) {
std::cerr << "Failed to open log file: " << log_file_path_ << std::endl;
}
}
public:
AsyncFileLogger() : running_(false) {}
// 启动日志系统
void start(const std::string& base_path = "") {
running_ = true;
// 初始化第一个日志文件
auto now = std::chrono::system_clock::now();
time_t now_time = std::chrono::system_clock::to_time_t(now);
struct tm* local_tm = localtime(&now_time);
char time_buf[64];
strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", local_tm);
log_file_path_ = base_path + "app_log_" + std::string(time_buf) + ".log";
log_file_.open(log_file_path_, std::ios::app);
if (!log_file_.is_open()) {
std::cerr << "Failed to open log file: " << log_file_path_ << std::endl;
}
// 启动工作线程
worker_thread_ = std::thread(&AsyncFileLogger::worker_func, this);
}
// 停止日志系统
void stop() {
running_ = false;
// 唤醒工作线程,使其退出循环
log_queue_.push(""); // 推送一个空日志唤醒等待的线程
if (worker_thread_.joinable()) {
worker_thread_.join();
}
}
// 对外提供的写日志接口
void write_log(const std::string& log) {
if (running_) {
log_queue_.push(log);
}
}
~AsyncFileLogger() {
if (running_) {
stop();
}
}
};
使用示例
下面是使用该异步日志系统的示例代码,主线程可以并发提交日志,不会阻塞主线程执行。
#include <iostream>
#include <vector>
#include <thread>
int main() {
AsyncFileLogger logger;
// 启动日志系统
logger.start();
// 主线程提交日志
logger.write_log("Main thread: program start");
// 模拟多个线程并发写日志
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back([i, &logger]() {
for (int j = 0; j < 10; ++j) {
logger.write_log("Thread " + std::to_string(i) + " write log: " + std::to_string(j));
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
});
}
// 主线程继续执行其他逻辑,不会被日志写入阻塞
for (int i = 0; i < 3; ++i) {
logger.write_log("Main thread: doing other work " + std::to_string(i));
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
// 等待所有子线程完成
for (auto& t : threads) {
if (t.joinable()) {
t.join();
}
}
logger.write_log("Main thread: program end");
// 停止日志系统
logger.stop();
return 0;
}
注意事项
在实际使用中需要注意以下几点:
- 日志队列的push操作如果队列无上限,极端情况下可能导致内存占用过高,可以根据需求给队列添加最大长度限制,超出时可以选择丢弃日志或者阻塞生产者
- 日志文件的路径需要确保有写入权限,文件滚动策略可以根据实际需求调整,比如按天滚动或者按大小滚动
- 程序退出前必须调用stop方法,否则可能导致部分日志没有写入文件就丢失
- 如果日志内容包含特殊字符,需要根据需求做转义处理,避免写入文件后出现格式问题
C++11_thread异步非阻塞文件日志系统日志队列修改时间:2026-07-11 02:57:50