异步日志系统
AsynLogging
类,核心功能是通过后台线程异步写入日志,避免前端线程因 IO 操作阻塞,提升系统性能。
一、核心设计思路
异步日志的核心是 “前端生产日志,后端消费日志”的分离模式:
- 前端线程(业务线程)通过
append
方法将日志写入内存缓冲区,操作高效(无磁盘 IO)。 - 后端线程(日志线程)定期或被唤醒后,将缓冲区中的日志批量写入磁盘文件。
2. 成员变量(注释中声明的核心变量)
变量名 | 作用 |
---|---|
flushInterval_ | 日志刷新间隔(如 3 秒,后端线程定期刷新) |
running_ | 原子变量,标记日志系统是否运行(线程安全的状态控制) |
basename_ | 日志文件基础名称(用于生成滚动日志文件名) |
rollSize_ | 日志文件滚动阈值(超过该大小则创建新文件,复用LogFile 的滚动逻辑) |
pthread_ | 后台日志线程的智能指针 |
mutex_ + cond_ | 互斥锁 + 条件变量,用于前端与后端线程的同步 |
currentBuffer_ | 前端当前使用的日志缓冲区(字符串形式) |
buffers_ | 已填满的缓冲区队列(等待后端线程处理) |
output_ | LogFile 对象,负责实际的日志文件写入和滚动管理 |
latch_ | 倒计时门闩(用于确保后台线程启动完成后,前端才开始写入日志) |
AsynLogging::append
方法中判断是否需要切换当前日志缓冲区的核心条件,用于决定是否将当前缓冲区(currentBuffer_
)移入待处理队列并创建新缓冲区,具体解析如下:
条件逻辑
if (currentBuffer_.size() >= BufMaxLen ||currentBuffer_.capacity() - currentBuffer_.size() < len)
- 两个判断条件用
||
(逻辑或)连接,满足任意一个即触发缓冲区切换。
条件 1:currentBuffer_.size() >= BufMaxLen
- 含义:当前缓冲区的已使用大小(
size()
)大于等于预设的最大容量(BufMaxLen = 4KB
)。 - 作用:确保单个缓冲区不会无限增长,当达到上限时,强制将其移入待处理队列(
buffers_
),由后端线程写入磁盘。
条件 2:currentBuffer_.capacity() - currentBuffer_.size() < len
- 含义:当前缓冲区的剩余可用空间(总容量
capacity()
减去已使用大小size()
)小于本次要写入的日志长度(len
)。 - 作用:避免因 “剩余空间不足” 导致日志被截断或缓冲区被迫扩容。即使缓冲区未填满(未达
BufMaxLen
),但剩余空间不够写入当前日志时,也会提前切换缓冲区,保证日志完整性。
触发后的操作
当上述条件满足时,代码会执行:
buffers_.push_back(std::move(currentBuffer_)); // 将当前缓冲区移入待处理队列
currentBuffer_.reserve(BufMaxLen); // 重置新缓冲区,预留最大容量
- 通过
std::move
转移当前缓冲区的所有权到队列,避免拷贝开销。 - 新缓冲区预分配
BufMaxLen
大小的空间,减少后续写入时的内存分配次数。
设计目的
- 保证日志完整性:避免因空间不足导致日志片段化或丢失。
- 控制内存占用:单个缓冲区大小不超过
4KB
,防止内存过度消耗。 - 减少 IO 次数:缓冲区满或空间不足时才切换,实现批量写入,提升效率。
void AsynLogging::append(const char *msg, const size_t len)
{std::unique_lock<std::mutex> lock(mutex_); // 加锁保证线程安全// 若当前缓冲区不足(已满或剩余空间不够),则将其移入队列,换新缓冲区if (currentBuffer_.size() >= BufMaxLen || currentBuffer_.capacity() - currentBuffer_.size() < len){buffers_.push_back(std::move(currentBuffer_)); // 转移当前缓冲区所有权currentBuffer_.reserve(BufMaxLen); // 重置新缓冲区}currentBuffer_.append(msg, len); // 写入日志到当前缓冲区cond_.notify_all(); // 唤醒后端线程处理缓冲区
}
- 核心作用:前端线程写入日志到内存缓冲区,避免直接写磁盘。
- 线程安全:通过
mutex_
加锁,支持多线程并发写入。 - 缓冲区切换:当当前缓冲区不足时,将其移入待处理队列,创建新缓冲区继续写入。
- 唤醒后端:写入后通知后端线程有数据待处理。
倒计时门闩(CountDownLatch) 类
用于多线程同步场景,核心功能是等待一个或多个线程完成特定操作后,再继续执行当前线程。
一、类的核心成员(注释中声明)
count_
:倒计时计数器,初始化为指定值,每调用一次countDown()
减 1,直至为 0。mutex_
:互斥锁,用于保护count_
的线程安全访问。cond_
:条件变量,用于线程间的等待与唤醒机制。
二、核心方法解析
1. 构造函数 CountDownLatch(int count)
CountDownLatch::CountDownLatch(int count) : count_(count) {}
- 初始化
count_
为传入的计数初始值(例如,若需要等待 3 个线程完成,则初始化为 3)。
2. wait()
方法
void CountDownLatch::wait()
{std::unique_lock<std::mutex> lock(mutex_); // 加锁,确保线程安全while(count_ > 0) // 循环判断,避免虚假唤醒{cond_.wait(lock); // 释放锁并阻塞等待,被唤醒时重新获取锁}
}
- 作用:调用该方法的线程会阻塞,直到
count_
减为 0 才继续执行。 - 线程安全:通过
std::unique_lock
加锁,保证对count_
的访问互斥。 - 防止虚假唤醒:使用
while
循环而非if
判断,确保被唤醒后再次检查count_
是否真的为 0(条件变量可能因系统原因虚假唤醒)。
3. countDown()
方法
void CountDownLatch::countDown()
{std::unique_lock<std::mutex> lock(mutex_); // 加锁count_ -= 1; // 计数器减1if(count_ == 0) // 当计数器归0时{cond_.notify_all(); // 唤醒所有等待的线程}
}
- 作用:每调用一次,计数器
count_
减 1;当count_
变为 0 时,唤醒所有通过wait()
阻塞的线程。 - 线程安全:加锁确保
count_
的修改是原子操作,避免多线程并发修改导致的计数错误。
4. getCount()
方法
int CountDownLatch::getCount() const
{std::unique_lock<std::mutex> lock(mutex_); // 加锁return count_; // 返回当前计数器值
}
- 作用:获取当前
count_
的数值(线程安全的访问)。
三、典型使用场景
倒计时门闩常用于以下同步场景:
- 主线程等待子线程初始化:例如,主线程启动 N 个子线程后,调用
wait()
阻塞,每个子线程初始化完成后调用countDown()
,当所有子线程初始化完毕(count_
归 0),主线程被唤醒继续执行。 - 协调多个线程完成任务:例如,多个线程完成各自任务后调用
countDown()
,最后一个线程完成时唤醒等待的线程进行汇总操作。
四、核心设计思想
- 线程同步:通过互斥锁(
mutex_
)保护共享变量count_
,通过条件变量(cond_
)实现线程间的等待 / 唤醒。 - 计数器机制:用
count_
跟踪待完成的操作数量,归 0 时触发同步点。 - 安全性:避免了多线程并发修改计数器的竞态条件,且通过循环判断防止条件变量的虚假唤醒。
3. 为什么需要reserve?
3.1 移动后的状态不确定性
std::vector<int> currentBuffer_(BufMaxLen);
// ... 填充数据// 移动后状态不确定
buffers_.push_back(std::move(currentBuffer_));// 此时 currentBuffer_ 可能是:
// 情况1: size=0, capacity=0 (需要重新分配)
// 情况2: size=0, capacity=BufMaxLen (理想情况)
// 情况3: 其他未指定状态// 为保证一致性,显式reserve
currentBuffer_.reserve(BufMaxLen);
std::move后原来的空间确实可能丢失,这正是需要reserve的关键原因。
1. std::move后的不确定性
标准规定:
被移动后的对象处于有效但未指定状态
实现可以自由选择如何处置被移动的对象
实际可能的情况:
std::vector<int> currentBuffer_(1000); // 容量1000// 移动操作后,currentBuffer_ 可能:
auto movedBuffer = std::move(currentBuffer_);// 情况1: 容量清零(常见实现)
// currentBuffer_.capacity() == 0// 情况2: 容量保留(某些优化)
// currentBuffer_.capacity() == 1000// 情况3: 其他任意有效状态
2. 具体验证代码
#include <vector>
#include <iostream>void demonstrateMoveUncertainty() {std::vector<int> buffer(1000, 42); // 容量1000std::cout << "移动前 - Size: " << buffer.size() << ", Capacity: " << buffer.capacity() << std::endl;std::vector<int> newBuffer = std::move(buffer);std::cout << "移动后 - Size: " << buffer.size() << ", Capacity: " << buffer.capacity() << std::endl;// 不同编译器的可能输出:// GCC: 移动后 - Size: 0, Capacity: 0// Clang: 移动后 - Size: 0, Capacity: 0 // MSVC: 移动后 - Size: 0, Capacity: 1000 (可能保留)
}
3. 为什么空间会丢失?
移动语义的实现选择:
// vector移动构造函数的可能实现之一
vector(vector&& other) noexcept : size_(other.size_), capacity_(other.capacity_), data_(other.data_)
{// 标准允许:可以清零原对象other.size_ = 0;other.capacity_ = 0; // 这里可能清零容量!other.data_ = nullptr;
}// 或者另一种实现:
vector(vector&& other) noexcept : size_(other.size_), capacity_(other.capacity_) , data_(other.data_)
{// 也可能保留原对象的容量other.size_ = 0;// other.capacity_ 保持不变other.data_ = nullptr;
}