整体设计思路
异步日志器的核心目标是解耦业务线程的日志写入和磁盘 IO 操作,避免业务线程被慢速的磁盘 IO 阻塞。整体流程:
核心特点:
-
前端(业务线程)只做内存操作(
append到currentBuffer_),速度极快; -
后台线程单独处理磁盘 IO,支持 "定时刷盘(flushInterval_秒)" 和 "缓冲区满触发刷盘";
-
用
std::mutex+std::condition_variable实现线程同步,用std::atomic保证线程安全的状态标记。
核心模块拆解(按文件结构)
1. 头文件 AsyncLogging.hpp:类的接口与成员定义
cpp
#ifndef ASYNCLOGGING_HPP
#define ASYNCLOGGING_HPP
#include "LogFile.hpp" // 日志文件操作类(负责实际写磁盘)
#include<thread> // 后台线程
#include<atomic> // 原子变量(线程安全的状态标记)
#include<condition_variable> // 条件变量(线程同步)
#include<vector> // 缓冲区队列
namespace wangt
{
class AsyncLogging
{
private:
// 禁用拷贝构造和赋值运算符:单例/独占资源的类必须禁用,避免资源重复释放
AsyncLogging(const AsyncLogging &) = delete;
AsyncLogging &operator=(const AsyncLogging &) = delete;
private:
void workthreadfunc(); // 后台工作线程的核心函数(实际执行刷盘)
private:
// 核心成员变量(按功能分类)
// 1. 刷盘配置
const int flushInterval_; // 定时刷盘间隔(秒),构造时确定不可修改
std::atomic<bool> running_; // 后台线程运行状态(原子变量,线程安全)
const std::string basename_; // 日志文件前缀(如"wangt.log")
const size_t rollSize_; // 日志文件回滚大小(超过该大小新建文件)
// 2. 线程相关
std::unique_ptr<std::thread> pthread_; // 后台线程(智能指针自动管理生命周期)
std::mutex mutex_; // 保护缓冲区的互斥锁
std::condition_variable cond_; // 唤醒后台线程的条件变量
// 3. 缓冲区相关
std::string currentBuffer_; // 前端当前写入的缓冲区(单缓冲区)
std::vector<std::string> buffers_; // 待刷盘的缓冲区队列
// 4. 磁盘写入
wangt::LogFile output_; // 日志文件操作类(封装fopen/fwrite/fflush等)
public:
// 构造函数:日志文件名前缀 + 回滚大小 + 刷盘间隔(默认3秒)
AsyncLogging(const std::string &basename, size_t rollSize, int flushInterval = 3);
~AsyncLogging(); // 析构时确保停止线程并刷盘
void append(const std::string &info); // 重载:接收std::string
void append(const char *info, int len); // 核心append:接收char*+长度(避免拷贝)
void start(); // 启动后台线程
void stop(); // 停止后台线程
void flush(); // 强制刷盘(立即写入磁盘)
};
}
#endif
关键成员说明:
-
std::atomic<bool> running_:替代普通 bool,保证多线程下 "运行状态" 的读写无竞争(比如业务线程调用 stop () 修改 running_,后台线程循环判断 running_); -
std::unique_ptr<std::thread> pthread_:智能指针自动释放线程资源,避免手动管理的内存泄漏; -
currentBuffer_:前端业务线程唯一的写入缓冲区,减少锁竞争(所有业务线程都写这个缓冲区,锁保护); -
buffers_:待刷盘的缓冲区队列,满了之后后台线程批量处理。
2. 实现文件 AsyncLogging.cpp:核心逻辑
(1)静态常量与构造 / 析构函数
cpp
namespace wangt
{
// 缓冲区配置常量(前端缓冲区最大长度4000字节,队列最大容量16个缓冲区)
static const int BufMaxLen = 4000;
static const int BufQueueSize = 16;
// 构造函数:初始化配置和缓冲区
AsyncLogging::AsyncLogging(const std::string &basename, size_t rollSize, int flushInterval)
: flushInterval_(flushInterval), // 刷盘间隔
running_(false), // 初始状态:未运行
rollSize_(rollSize), // 日志文件回滚大小
pthread_(nullptr), // 线程初始化为空
output_(basename, rollSize, false) // 初始化日志文件操作类
{
// 预分配缓冲区空间:避免string/vector自动扩容(扩容会触发内存分配,降低性能)
currentBuffer_.reserve(BufMaxLen);
buffers_.reserve(BufQueueSize);
}
// 析构函数:确保线程停止+最后一次刷盘
AsyncLogging::~AsyncLogging()
{
if (running_)
{
stop(); // 先停止线程
}
flush(); // 析构时强制刷盘,避免缓冲区残留日志丢失
}
关键设计:
reserve预分配空间:currentBuffer_.reserve(BufMaxLen)让字符串直接预留 4000 字节空间,后续 append 无需频繁扩容;- 析构函数的 "兜底刷盘":即使业务代码忘记调用 stop ()/flush (),析构时也会保证最后一批日志写入磁盘。
(2)线程启停(start/stop)
cpp
// 启动后台线程
void AsyncLogging::start()
{
running_ = true; // 标记为运行状态
// 初始化后台线程:绑定workthreadfunc函数,this为成员函数的隐含参数
pthread_.reset(new std::thread(&AsyncLogging::workthreadfunc, this));
}
// 停止后台线程
void AsyncLogging::stop()
{
running_ = false; // 标记为停止状态
cond_.notify_all(); // 唤醒后台线程(避免线程卡在wait_for)
pthread_->join(); // 等待后台线程执行完毕(避免主线程退出导致线程崩溃)
output_.flush(); // 停止后强制刷盘
}
关键注意:
-
cond_.notify_all():必须唤醒后台线程,否则后台线程可能卡在cond_.wait_for,无法检测到running_ = false,导致pthread_->join()阻塞; -
pthread_->join():等待线程完全退出,避免线程资源泄漏。
(3)核心:append 方法(业务线程写入日志)
cpp
// 重载:接收std::string,转调char*版本(减少代码冗余)
void AsyncLogging::append(const std::string &info)
{
append(info.c_str(), info.size());
}
// 核心append:接收char*+长度(最底层,避免string拷贝)
void AsyncLogging::append(const char *info, int len)
{
// 加锁:保护currentBuffer_和buffers_的线程安全(多业务线程同时append)
std::unique_lock<std::mutex> _lock(mutex_);
// 计算当前缓冲区剩余空间
size_t remaining = currentBuffer_.capacity() - currentBuffer_.size();
// 判断:剩余空间不足,需要切换缓冲区
if (currentBuffer_.size() >= BufMaxLen || remaining < static_cast<size_t>(len))
{
// 将当前缓冲区移入队列(std::move避免拷贝:string的move是浅拷贝,仅转移指针)
buffers_.push_back(std::move(currentBuffer_));
// 重置当前缓冲区:清空+重新预分配空间
currentBuffer_.clear();
currentBuffer_.reserve(BufMaxLen);
}
// 写入当前缓冲区(内存操作,极快)
currentBuffer_.append(info, len);
// 唤醒后台线程:有新日志,可触发刷盘(无需等定时)
cond_.notify_all();
}
核心亮点:
-
std::move(currentBuffer_):string 的 move 语义避免了大字符串的拷贝(4000 字节的拷贝会消耗性能),仅转移字符串的内部指针; -
锁的粒度:仅在 "判断缓冲区 + 写入缓冲区" 时加锁,锁粒度极小,对业务线程性能影响可忽略;
-
cond_.notify_all():每次 append 都唤醒后台线程,保证日志能尽快刷盘(后台线程判断如果缓冲区没满,会继续 wait_for)。
(4)后台线程核心:workthreadfunc(批量刷盘)
cpp
void AsyncLogging::workthreadfunc()
{
std::vector<std::string> buffersToWrite; // 线程局部队列(减少锁竞争)
buffersToWrite.reserve(BufQueueSize); // 预分配空间
// 后台线程主循环:只要running_为true,就持续运行
while (running_)
{
{
// 加锁:仅在操作缓冲区时加锁
std::unique_lock<std::mutex> _lock(mutex_);
// 等待条件:
// 1. 缓冲区队列非空(有日志要刷);
// 2. 超时(flushInterval_秒到);
// 3. running_为false(线程要停止)
if (buffers_.empty())
{
cond_.wait_for(_lock, std::chrono::seconds(flushInterval_));
}
// 无论因何唤醒,都将当前缓冲区移入队列(即使没满,定时刷盘)
buffers_.push_back(std::move(currentBuffer_));
// 重置当前缓冲区
currentBuffer_.clear();
currentBuffer_.reserve(BufMaxLen);
// 交换缓冲区:将buffers_(待刷盘)交换到本地buffersToWrite
// 交换操作是O(1),且交换后buffers_为空,释放锁后不影响前端append
buffersToWrite.swap(buffers_);
// 重置buffers_的预留空间
buffers_.reserve(BufQueueSize);
} // 解锁:后续刷盘操作无需加锁,提升并发性能
// 保护机制:如果待刷盘缓冲区过多(生产速度>消费速度),丢弃多余日志
if (buffersToWrite.size() > 25)
{
char buf[256];
snprintf(buf, sizeof buf, "Dropped log messages at largerbuffers\n");
fputs(buf, stderr); // 输出丢弃日志的提示到标准错误
// 仅保留前2个缓冲区,丢弃其余(避免内存溢出)
buffersToWrite.erase(buffersToWrite.begin() + 2, buffersToWrite.end());
}
// 批量刷盘:遍历缓冲区,写入磁盘
for (const auto &buffer : buffersToWrite)
{
output_.append(buffer.c_str(), buffer.size()); // 调用LogFile写磁盘
}
// 清空本地队列,准备下一次刷盘
buffersToWrite.clear();
// 强制刷盘:确保写入操作系统缓冲区的日志刷到物理磁盘
output_.flush();
}
// 线程退出前,最后一次刷盘(避免残留日志)
output_.flush();
}
核心设计:
-
buffersToWrite.swap(buffers_):交换操作是异步日志的经典优化 ------ 将待刷盘的缓冲区交换到线程局部变量,解锁后再刷盘,最大化减少锁持有时间; -
cond_.wait_for:实现 "定时刷盘"+"触发刷盘" 双重逻辑,既保证日志不会积压太久(最多 flushInterval_秒),又能在有日志时立即刷盘; -
过载保护:
buffersToWrite.size() > 25时丢弃日志,避免前端疯狂写日志导致内存溢出(生产环境必备)。
(5)强制刷盘:flush 方法
cpp
void AsyncLogging::flush()
{
std::vector<std::string> buffersToWrite;
std::unique_lock<std::mutex> _lock(mutex_);
// 将当前缓冲区移入队列
buffers_.push_back(std::move(currentBuffer_));
// 交换缓冲区到本地
buffersToWrite.swap(buffers_);
// 批量写入磁盘
for (const auto &buffer : buffersToWrite)
{
output_.append(buffer.c_str(), buffer.size());
}
// 强制刷盘到物理磁盘
output_.flush();
// 清空本地队列
buffersToWrite.clear();
}
}
作用:业务代码可主动调用 flush (),强制将所有缓冲区的日志写入磁盘(比如程序退出前、关键操作后)。
三、关键设计亮点与注意事项
1. 性能优化点
| 优化手段 | 作用 |
|---|---|
reserve预分配空间 |
避免 string/vector 频繁扩容,减少内存分配开销 |
std::move转移缓冲区 |
避免大字符串拷贝,提升 append 性能 |
| 锁粒度最小化 | 仅在操作缓冲区时加锁,刷盘时解锁,减少线程阻塞 |
| 缓冲区交换(swap) | O (1) 操作,快速转移待刷盘数据,减少锁持有时间 |
| 批量刷盘 | 减少磁盘 IO 次数(磁盘 IO 单次耗时高,批量效率高) |
2. 线程安全保障
std::atomic<bool> running_:线程安全的状态标记,避免多线程读写竞争;std::mutex保护缓冲区:所有对currentBuffer_/buffers_的操作都加锁;std::condition_variable:实现线程的高效等待 / 唤醒,避免忙等(CPU 空转)。
3. 待完善 / 注意点
- 缺少空指针 / 长度校验:
append(const char *info, int len)未判断info == nullptr或len <= 0,可能导致崩溃; basename_初始化缺失:原代码构造函数中未初始化basename_(已在之前的修复中补充),会导致 LogFile 创建失败;- 日志格式:当前仅写入原始日志内容,缺少时间、级别、文件 / 行号等上下文(需结合 Logger 类补充);
- 异常处理:未处理 LogFile 写磁盘失败的情况(如磁盘满、权限不足)。
总结
- 核心架构:前端内存缓冲区 + 后台线程批量刷盘,彻底解耦业务线程和磁盘 IO;
- 性能核心:最小化锁粒度、批量 IO、move 语义、预分配空间,保证高并发下的低延迟;
- 可靠性保障:过载保护(丢弃多余日志)、析构刷盘、停止线程时的兜底处理,避免日志丢失;
- 适用场景:高并发的后端服务(如服务器、中间件),需要高性能、低阻塞的日志输出。
整体代码讲解
cpp
#ifndef ASYNCLOGGING_HPP
#define ASYNCLOGGING_HPP
#include "LogFile.hpp"
#include<thread>
#include<atomic>
#include<condition_variable>
#include<vector>
namespace wangt
{
class AsyncLogging
{
private:
AsyncLogging(const AsyncLogging &) = delete;
AsyncLogging &operator=(const AsyncLogging &) = delete;
private:
void workthreadfunc(); // 工作线程
private:
const int flushInterval_; // 定期(flushInterval_秒)将缓冲区的数据写
std::atomic<bool> running_; // 是否正在运行
const string basename_; // 日志filename名字
const size_t rollSize_; // 回滚大小
std::unique_ptr<std::thread> pthread_; // 执行该异步日志记录器的线程
std::mutex mutex_; //
std::condition_variable cond_; //
std::string currentBuffer_; // 当前的缓冲区
std::vector<std::string> buffers_; // 数据缓冲区队列
wangt::LogFile output_; // 定义日志文件对象
public:
// rollSize //回滚大小
// flushInterval = 3 ; // 刷新间隔,默认值3秒
AsyncLogging(const string &basename, size_t rollSize, int flushInterval = 3);
~AsyncLogging();
void append(const string &info);
void append(const char *info, int len);
void start();
void stop();
void flush();
};
}
#endif#include "AsyncLogging.hpp"
namespace wangt
{
static const int BufMaxLen = 4000;
static const int BufQueueSize = 16;
AsyncLogging::AsyncLogging(const std::string &basename, size_t rollSize, int flushInterval)
: flushInterval_(flushInterval), // 刷新间隔
running_(false),
rollSize_(rollSize),
pthread_(nullptr),
// latch_(1),
output_(basename, rollSize, false) // 定义日志文件对象
{
currentBuffer_.reserve(BufMaxLen);
buffers_.reserve(BufQueueSize); // vector预定大小,避免自动增长(效率更高)
}
AsyncLogging::~AsyncLogging()
{
if (running_)
{
stop();
}
flush();
}
void AsyncLogging::start()
{
running_ = true;
// 执行该异步日志记录器的线程
pthread_.reset(new std::thread(&AsyncLogging::workthreadfunc, this));
// latch_.wait();
}
void AsyncLogging::stop()
{
running_ = false;
cond_.notify_all();
pthread_->join();
output_.flush();
}
void AsyncLogging::append(const string &info)
{
append(info.c_str(), info.size());
}
void AsyncLogging::append(const char *info, int len)
{
std::unique_lock<std::mutex> _lock(mutex_);
if (currentBuffer_.size() >= BufMaxLen ||
(currentBuffer_.capacity() - currentBuffer_.size()) < len)
{
buffers_.push_back(std::move(currentBuffer_));
currentBuffer_.clear();
currentBuffer_.reserve(BufMaxLen);
}
else
{
currentBuffer_.append(info, len);
}
cond_.notify_all();
}
void AsyncLogging::workthreadfunc()
{
// tulun::LogFile output(basename_,rollSize_,false); //定义日志文件对象
std::vector<std::string>
buffersToWrite; // 线程函数的局部队列
// latch_.countDown();
while (running_)
{
// std::this_thread::sleep_for(std::chrono::milliseconds(5000));
{
std::unique_lock<std::mutex> _lock(mutex_);
if (buffers_.empty())
{
cond_.wait_for(_lock,
std::chrono::seconds(flushInterval_));
// 时间点到达 ,还要获得mutex_ 才能从wait_for 函数返回;
}
// 无论cond是因何(一是超时,二是当前缓冲区写满了)而醒来,都要将currentBuffer_放到buffers_中。 测试
// 如果是因为时间到(3秒)而醒,那么currentBuffer_还没满,此时也要将之写入LogFile中。
// 如果已经有一个前端buffer满了,那么在前端线程中就已经把一个前端buffer放到buffers_中了。
// 此时,还是需要把currentBuffer_放到buffers_中(注意,前后放置是不同的buffer,
// 因为在前端线程中,currentBuffer_已经被换成nextBuffer_指向的 buffer了)。
buffers_.push_back(std::move(currentBuffer_));
currentBuffer_.reserve(BufMaxLen);
buffersToWrite.swap(buffers_);
buffers_.reserve(BufQueueSize);
// 释放mutex_ ;
}
////异步写文件
// 如果将要写入文件的buffer列表中buffer的个数大于25,那么将多余数据删 除。
// 前端陷入死循环,拼命发送日志消息,超过后端的处理能力,这是典型的生产速 度超过消费速度,
// 会造成数据在内存中的堆积,严重时引发性能问题(可用内存不足)或程序崩溃(分配内存失败)。
if (buffersToWrite.size() > 25)
{
char buf[256];
snprintf(buf, sizeof buf, "Dropped log messages at largerbuffers\n");
fputs(buf, stderr);
// 丢掉多余日志,以腾出内存,仅保留两块缓冲区
buffersToWrite.erase(buffersToWrite.begin() + 2,
buffersToWrite.end());
}
// 将buffersToWrite的数据写入到日志文件中
for (const auto &buffer : buffersToWrite)
{
output_.append(buffer.c_str(), buffer.size());
}
buffersToWrite.clear();
}
output_.flush();
}
void AsyncLogging::flush()
{
std::vector<std::string> buffersToWrite; //
std::unique_lock<std::mutex> _lock(mutex_);
buffers_.push_back(std::move(currentBuffer_));
buffersToWrite.swap(buffers_);
for (const auto &buffer : buffersToWrite)
{
output_.append(buffer.c_str(), buffer.size());
}
output_.flush();
buffersToWrite.clear();
}
}
