前言
在高并发、高性能后端服务中,同步日志会严重拖慢业务线程。
为了解决这个问题,工业级主流方案都是:业务线程 → 写内存 → 后台线程异步落盘 这就是 异步日志(Async Logging)。
一、异步日志核心设计思想
1. 为什么要用异步日志?
-
同步日志:业务线程直接写文件,阻塞 IO,高并发下性能暴跌
-
异步日志 :业务线程只写内存,后台线程异步刷盘,业务无阻塞
2. 核心架构(你代码的设计)
CountDownLatch:线程同步门栓
AsyncLogging:异步日志核心(双缓冲、生产者 - 消费者)
LogFile / AppendFile:文件落地
Logger / LogMessage:日志格式化与宏封装
多线程安全:mutex + condition_variable
二、核心组件讲解
CountDownLatch 线程同步门栓
作用:让主线程等待后台线程启动完成再继续执行。
CountDownLatch.hpp
cpp
#include <mutex>
#include <condition_variable>
using namespace std;
#ifndef COUNT_DOWN_LATCH_HPP
#define COUNT_DOWN_LATCH_HPP
namespace tulun
{
class CountDownLatch
{
private:
int count_;
mutable std::mutex mutex_;
std::condition_variable cond_;
public:
CountDownLatch(int count);
~CountDownLatch() = default;
void wait();
void countDown();
int getCount() const;
};
} // namespace tulun
#endif
CountDownLatch.cpp
cpp
#include "CountDownLatch.hpp"
namespace tulun
{
// class CountDownLatch
// int count_;
// std::mutex mutex_;
// std::condition_variable cond_;
CountDownLatch::CountDownLatch(int count)
: count_(count)
{
}
void CountDownLatch::wait()
{
std::unique_lock<std::mutex> locker(mutex_);
while (count_ > 0)
{
cond_.wait(locker);
}
}
void CountDownLatch::countDown()
{
std::unique_lock<std::mutex> locker(mutex_);
count_-=1;
if(count_ == 0)
{
cond_.notify_all();
}
}
int CountDownLatch::getCount() const
{
std::unique_lock<std::mutex> locker(mutex_);
return count_;
}
} // namespace tulun
三、异步日志核心:AsyncLogging
这是整个日志系统的心脏。
核心设计:双缓冲机制(Double Buffer)
-
currentBuffer_:当前写入缓冲区
-
buffers_:装满的缓冲区队列
-
后台线程:批量将缓冲区写入文件
核心流程
业务线程调用
append()→ 写入 currentBuffer缓冲区满 → 放入 buffers 队列
条件变量通知后台线程
后台线程批量写入文件
四、AsyncLogging 完整代码解析
AsyncLogging.hpp
cpp
#include "AppendFile.hpp"
#include "LogFile.hpp"
#include "CountDownLatch.hpp"
#include <thread>
#include <atomic>
#include <string>
#include <memory>
#include <mutex>
#include <condition_variable>
#include <deque>
#include <vector>
using namespace std;
#ifndef ASYN_LOGGING_HPP
#define ASYN_LOGGING_HPP
namespace tulun
{
class AsynLogging
{
private:
void workthreadfunc(); // 【后端线程函数】真正写文件的人
private:
const int flushInterval_; // 多久自动刷盘(3秒)
std::atomic<bool> running_; // 日志是否在运行(控制线程退出)
const std::string basename_; // 日志文件名前缀(如:syrou)
const size_t rollSize_; // 日志多大滚动(默认2M)
tulun::CountDownLatch latch_;
std::unique_ptr<std::thread> pthread_; // 后端线程(写文件的线程)
std::mutex mutex_; // 锁:保护队列和缓冲区
std::condition_variable cond_; // 条件变量:通知后端"有数据啦快来写"
std::string currentBuffer_; // 当前正在写的缓冲(4K)
//std::deque<std::string> buffers_; // 满了的缓冲队列(多个4K)
std::vector<std::string> buffers_;
tulun::LogFile output_; // 真正写文件的对象(你之前写的LogFile)
public:
AsynLogging(const std::string &basename,
const size_t rollsize = 1024 * 1024 * 2,
int flushInterval = 3);
~AsynLogging();
AsynLogging(const AsynLogging &) = delete;
AsynLogging &operator=(const AsynLogging &) = delete;
void append(const std::string &msg);
void append(const char *msg, const size_t len);// 前端打日志调用
void start();// 启动后端线程
void stop();
void flush();
};
}
#endif
AsyncLogging.cpp 核心方法解析
cpp
#include "AsynLogging.hpp"
namespace tulun
{
const size_t BufMaxLen = 1024 * 8; // 4k
const size_t BufQueueSize = 32;
// class AsynLogging
// const int flushInterval_; // 3s;
// std::atomic<bool> running_;
// const std::string basename_;
// const size_t rollSize_; // 10M;
// std::unique_ptr<std::thread> pthread_;
// std::mutex mutex_;
// std::condition_variable cond_;
// std::string currentBuffer_; // 4K
// std::deque<std::string> buffers_;
// std::vector<std::string> buffers_;
// tulun::LogFile output_;
void AsynLogging::workthreadfunc()
{
//std::deque<std::string> buffersToWrite;
std::vector<std::string> buffersToWrite; // 准备写入文件的缓冲
latch_.countDown();
while (running_) // 一直跑
{
{
std::unique_lock<std::mutex> locker(mutex_); // 加锁
// 没有数据 && 还在运行 → 等待1秒
while (buffers_.empty() && running_)
{
cond_.wait_for(locker, std::chrono::seconds(1));
}
// 把当前缓冲也加入队列
buffers_.push_back(std::move(currentBuffer_));
currentBuffer_.reserve(BufMaxLen);
// 交换队列!【最关键一步】
buffersToWrite.swap(buffers_);
buffers_.reserve(BufQueueSize);
}
// 解锁后才写文件,不阻塞前端!!!
if(buffersToWrite.size() > 50)
{
fprintf(stderr,"Dropped log message at larger buffers \n");
buffersToWrite.erase(buffersToWrite.begin()+2,buffersToWrite.end());
}
// 把所有缓冲写入文件
for (const auto &buf : buffersToWrite)
{
output_.append(buf);
}
buffersToWrite.clear();
}
output_.flush();
}
// 参数列表里,只放【需要从外面传进来的值】
AsynLogging::AsynLogging(const std::string &basename,
const size_t rollsize,
int flushInterval)
: basename_(basename),
flushInterval_(flushInterval),
rollSize_(rollsize),
running_(false),
pthread_(nullptr),
mutex_{},
cond_{},
output_(basename, rollsize, flushInterval),
latch_{1}
{
currentBuffer_.reserve(BufMaxLen);
//
}
AsynLogging::~AsynLogging()
{
if (running_)
{
stop();
}
}
void AsynLogging::append(const std::string &msg)
{
append(msg.c_str(), msg.size());
}
void AsynLogging::append(const char *msg, const size_t len)
{
std::unique_lock<std::mutex> locker(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(); // 唤醒后端线程:来活啦!
}
void AsynLogging::start()
{
running_ = true;
pthread_.reset(new std::thread(&AsynLogging::workthreadfunc, this));
latch_.wait();
}
void AsynLogging::stop()
{
running_ = false;
cond_.notify_all();
pthread_->join();
}
void AsynLogging::flush()
{
//std::deque<std::string> bufferToWriter;
std::vector<std::string> bufferToWriter;
std::unique_lock<std::mutex> locker(mutex_);
buffers_.push_back(std::move(currentBuffer_));
currentBuffer_.reserve(BufMaxLen);
bufferToWriter.swap(buffers_);
buffers_.reserve(BufQueueSize);
for (const auto &buf : bufferToWriter)
{
output_.append(buf);
}
output_.flush();
bufferToWriter.clear();
}
}
五、使用示例(Test04_11_Asyn.cpp)
cpp
#include <iostream>
#include <string>
using namespace std;
#include "LogCommon.hpp"
#include "Logger.hpp"
#include "AsynLogging.hpp"
//tulun::LogFile asynfile("hm");
const int n = 10000;
void funa()
{
for (int i = 0; i < n; ++i)
{
//std::this_thread::sleep_for(std::chrono::milliseconds(10));
LOG_DEBUG << "syrou funa" << i;
}
}
void funb()
{
for (int i = 0; i < n; ++i)
{
//std::this_thread::sleep_for(std::chrono::milliseconds(10));
LOG_TRACE << "syr funb" << i;
}
}
void func()
{
LOG_INFO << "func";
}
// 创建一个全局异步日志对象
tulun::AsynLogging asynfile("syrou");
void asynFile(const std::string &msg)
{
asynfile.append(msg);
}
void asynFlush()
{
asynfile.flush();
}
int main()
{
// 1. 启动异步日志的【后台写文件线程】
asynfile.start();
// 2. 设置日志级别
tulun::Logger::setLogLevel(tulun::LOG_LEVEL::TRACE);
// 3. 把日志输出 → 绑定到异步日志
tulun::Logger::setOutput(asynFile);
// 4. 把刷新 → 绑定到异步日志
tulun::Logger::setFlush(asynFlush);
tulun::Timestamp start,end;
start = tulun::Timestamp::Now();
std::thread tha(funa);
std::thread thb(funb);
tha.join();
thb.join();
end = tulun::Timestamp::Now();
cout<<" "<<tulun::diffMicro(end,start)<<endl;
return 0;
}
六、高性能关键点总结(重点)
1. 双缓冲机制(零拷贝交换)
cpp
buffersToWrite.swap(buffers_);
不拷贝数据,只交换指针,速度极快。
2. 前端只写内存,无 IO 阻塞
业务线程完全不参与文件 IO,不影响业务性能。
3. 无锁后端写入
后台线程写文件时不加锁,极大提升吞吐量。
4. 条件变量 wait_for
即使没有日志,也会1 秒自动唤醒,防止日志滞留内存。
5. CountDownLatch 保证线程安全启动
确保后台线程完全启动后,主线程才继续执行。
6. 多线程安全
所有共享变量都由 mutex 保护。
七、异步日志 VS 同步日志
| 模式 | 性能 | 阻塞业务 | 适用场景 |
|---|---|---|---|
| 同步日志 | 低 | 是 | 小工具、测试程序 |
| 异步日志 | 极高 | 否 | 高并发服务器、网关、核心服务 |