【C++ 高性能日志系统实战】第三篇:异步日志系统的实现与优化

在上一篇文章中,我们完成了基础日志系统的搭建,实现了日志级别控制、日志格式封装、文件滚动输出等核心功能。但同步日志存在一个明显问题:日志写入操作会阻塞业务线程,尤其是在高并发场景下,频繁的磁盘 IO 会严重影响程序性能。因此,本文将基于原有框架,实现异步日志系统,通过 "生产 - 消费" 模型解耦日志写入与业务逻辑,大幅提升日志系统的性能。

一、异步日志核心设计思路

异步日志的核心是 "内存缓冲区 + 后台线程":

  1. 生产端:业务线程将日志内容写入内存缓冲区,不直接操作磁盘,保证业务线程快速返回;
  2. 消费端:单独的后台线程从缓冲区读取日志数据,批量写入磁盘,降低磁盘 IO 频率;
  3. 线程同步:通过互斥锁、条件变量实现生产端与消费端的同步,保证数据安全;
  4. 缓冲区管理:设置固定大小的缓冲区,缓冲区满时自动切换,避免内存溢出。

二、核心组件实现解析

本次新增的核心文件包括:AsynLogging.hpp(异步日志类头文件)、AsynLogging.cpp(异步日志实现)、CountDownLatch.hpp/cpp(倒计时门闩,用于线程同步),以及测试文件Test04_11_Asyn.cpp。

2.1 辅助组件:CountDownLatch(倒计时门闩)

倒计时门闩的作用是保证后台线程完全启动后,主线程再继续执行 ,避免主线程先写入日志但后台线程未就绪的问题。(注:本文核心聚焦异步日志,CountDownLatch 的实现逻辑可参考经典多线程同步模型,核心是基于std::mutexstd::condition_variable实现计数等待)。

2.2 核心类:AsynLogging(异步日志类)

2.2.1 类成员设计(AsynLogging.hpp)

先看类的核心成员定义,逐行解析设计思路:

cpp 复制代码
namespace tulun
{
    class AsynLogging
    {
    private:
        void workthreadfunc(); // 后台消费线程的核心函数

    private:
        const int flushInterval_; // 日志刷盘间隔(默认3s)
        std::atomic<bool> running_; // 线程运行标记(原子变量,保证线程安全)
        const std::string basename_; // 日志文件基础名
        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_; // 待刷盘的缓冲区队列
        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(); // 手动刷盘
    };
}

设计思路解析

  • running_使用std::atomic<bool>:原子变量无需加锁即可保证多线程读写安全,避免互斥锁的性能开销;
  • 缓冲区设计:currentBuffer_(当前写入缓冲区) + buffers_(待刷盘队列):业务线程只写currentBuffer_,满了就放入队列,避免直接操作队列带来的频繁锁竞争;
  • LogFile output_:复用之前实现的日志文件滚动、追加写入逻辑,减少重复代码;
  • 禁用拷贝构造 / 赋值:异步日志类是单例语义,拷贝会导致线程、锁等资源混乱,因此直接禁用。
2.2.2 构造函数与初始化(AsynLogging.cpp)
cpp 复制代码
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} // 倒计时门闩初始值1:等待后台线程启动
{
    currentBuffer_.reserve(BufMaxLen); // 预留4K缓冲区空间,避免频繁扩容
}

关键细节

  • currentBuffer_.reserve(BufMaxLen):提前预留 4K 内存(BufMaxLen = 1024*4),避免日志写入时字符串频繁扩容,提升性能;
  • latch_{1}:门闩初始计数为 1,后台线程启动后调用countDown(),主线程通过latch_.wait()等待计数为 0。
2.2.3 核心写入接口:append 方法

append 是业务线程写入日志的入口,核心逻辑是 "判断缓冲区是否够用,不够则切换缓冲区,最后写入数据":

cpp 复制代码
void AsynLogging::append(const char *msg, const size_t len)
{
    std::unique_lock<std::mutex> locker(mutex_); // 加锁保护缓冲区
    // 两种情况需要切换缓冲区:
    // 1. 当前缓冲区已达最大容量;2. 剩余空间不足以写入当前日志
    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(); // 唤醒消费线程(有数据需要刷盘)
}

核心优化点

  • 使用std::move转移缓冲区:buffers_.push_back(std::move(currentBuffer_)),避免字符串拷贝,提升性能;
  • 加锁范围最小化:仅在操作缓冲区时加锁,写入数据后立即解锁(unique_lock出作用域自动解锁);
  • 唤醒消费线程:每次写入后调用cond_.notify_all(),保证有数据时消费线程能及时处理。
2.2.4 后台消费线程:workthreadfunc 方法

这是异步日志的核心消费逻辑,后台线程循环读取缓冲区数据并写入磁盘:

cpp 复制代码
void AsynLogging::workthreadfunc()
{
    std::deque<std::string> buffersToWrite;
    latch_.countDown(); // 门闩计数-1,主线程可继续执行
    while (running_)
    {
        {
            std::unique_lock<std::mutex> locker(mutex_);
            // 等待条件:要么有数据,要么超时(1s),避免死等
            while (buffers_.empty() && running_)
            {
                cond_.wait_for(locker, std::chrono::seconds(1));
            }
            // 将当前缓冲区加入队列,准备刷盘
            buffers_.push_back(std::move(currentBuffer_));
            currentBuffer_.reserve(BufMaxLen);
            // 交换缓冲区队列:减少锁持有时间(交换操作是O(1))
            buffersToWrite.swap(buffers_);
        }// 解锁:刷盘操作不需要加锁,提升并发性能

        // 保护机制:如果待刷盘队列过大,丢弃部分日志(避免内存溢出)
        if(buffersToWrite.size() > 50)
        {
            fprintf(stderr,"Dropped log message at larger buffers \n");
            buffersToWrite.erase(buffersToWrite.begin()+2,buffersToWrite.end());
        }

        // 批量写入磁盘(复用LogFile的append方法)
        for (const auto &buf : buffersToWrite)
        {
            output_.append(buf);
        }
        buffersToWrite.clear(); // 清空已刷盘的缓冲区
    }
    output_.flush(); // 线程退出前,刷入剩余数据
}

设计思路拆解

  1. 线程启动确认latch_.countDown()通知主线程 "消费线程已就绪";
  2. 等待数据 / 超时cond_.wait_for设置 1s 超时,避免消费线程一直阻塞,即使无数据也能定期检查退出标记;
  3. 交换缓冲区队列buffersToWrite.swap(buffers_),将待刷盘的缓冲区转移到局部变量,解锁后再刷盘,最大化减少锁持有时间;
  4. 过载保护 :当buffersToWrite.size() > 50时,丢弃部分日志,避免缓冲区队列无限增长导致内存耗尽;
  5. 批量刷盘:遍历缓冲区队列,批量写入磁盘,相比单条写入大幅降低磁盘 IO 次数。
2.2.5 线程启停控制:start/stop 方法
cpp 复制代码
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(); // 等待消费线程退出
}

关键细节

  • start()latch_.wait():确保主线程在消费线程完全启动后再写入日志,避免 "生产先于消费";
  • stop()cond_.notify_all():即使消费线程处于等待状态,也能被唤醒并检查running_标记,正常退出。

2.3 测试文件:Test04_11_Asyn.cpp

测试文件验证异步日志的性能,核心逻辑是 "多线程写入大量日志,统计耗时":

cpp 复制代码
tulun::AsynLogging asynfile("yhping"); // 初始化异步日志对象
const int n = 100000; // 每个线程写入10万条日志

// 线程1:写入DEBUG级别日志
void funa()
{
    for (int i = 0; i < n; ++i)
    {
        LOG_DEBUG << "yhping funa" << i;
    }
}

// 线程2:写入TRACE级别日志
void funb()
{
    for (int i = 0; i < n; ++i)
    {
        LOG_TRACE << "yhp funb" << i;
    }
}

// 日志输出回调:将日志写入异步缓冲区
void asynFile(const std::string &msg)
{
   asynfile.append(msg);
}

// 日志刷盘回调:手动刷盘(可选)
void asynFlush()
{
   asynfile.flush();
}

int main()
{
    asynfile.start(); // 启动异步日志线程
    tulun::Logger::setLogLevel(tulun::LOG_LEVEL::TRACE); // 设置日志级别
    tulun::Logger::setOutput(asynFile); // 替换日志输出函数为异步写入
    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. 替换日志系统的输出回调:Logger::setOutput(asynFile),让所有日志写入异步缓冲区;
  2. 启动两个线程,各写入 10 万条日志,统计总耗时;
  3. 对比同步日志:异步日志的耗时会大幅降低(同步日志需等待磁盘 IO,异步仅需写入内存)。

2.4 编译配置:example/CMakeLists.txt

新增异步日志测试程序的编译配置:

bash 复制代码
# 新增异步日志测试程序
add_executable(testAsyn Test04_11_Asyn.cpp  ${SRC_LOG})
# 链接pthread库(多线程必须)
target_link_libraries(testAsyn pthread)

关键配置 :异步日志依赖多线程,必须链接pthread库,否则会报线程相关的未定义符号错误。

三、核心知识点总结

本次异步日志实现涉及的核心 C++ 知识点:

  1. 多线程同步std::mutex(互斥锁)、std::condition_variable(条件变量)、std::atomic(原子变量);
  2. 智能指针std::unique_ptr管理线程资源,避免内存泄漏;
  3. 移动语义std::move转移字符串缓冲区,减少拷贝开销;
  4. 生产 - 消费模型:解耦日志写入与磁盘 IO,提升并发性能;
  5. 锁优化:最小化锁持有时间,通过交换队列将刷盘操作移出锁范围。

四、性能对比与优化方向

4.1 性能对比

  • 同步日志:20 万条日志(双线程)耗时约 5-10 秒(取决于磁盘 IO 速度);
  • 异步日志:20 万条日志耗时约 100-200 毫秒(仅内存操作,刷盘由后台线程异步完成)。

4.2 后续优化方向

  1. 多缓冲区设计:增加备用缓冲区,避免单缓冲区切换时的锁竞争;
  2. 日志压缩:刷盘前对日志数据进行压缩,减少磁盘占用;
  3. 线程池替代单线程:高并发场景下,用线程池处理刷盘操作;
  4. 缓冲区预分配:提前分配多个缓冲区,避免运行时频繁 reserve。

五、总结

本文基于原有日志系统,实现了异步日志的核心逻辑,通过 "内存缓冲区 + 后台线程" 的生产 - 消费模型,彻底解决了同步日志的性能瓶颈。核心设计思路是 "让业务线程只写内存,后台线程批量刷盘",同时通过锁优化、移动语义等手段保证高性能,通过倒计时门闩、条件变量等保证线程安全。

至此,一套完整的高性能 C++ 日志系统已基本成型,涵盖了日志级别、格式封装、文件滚动、异步写入等核心功能,可直接应用于高性能服务端程序的开发。

一键三连私信我分享源码压缩包,可直接粘贴使用哦~

相关推荐
oi..2 小时前
CSRF安全攻防:Referer 校验与 Token 防护详解
前端·网络·笔记·测试工具·安全·网络安全·csrf
qq_260241232 小时前
将盾CDN:Web应用防火墙(WAF)的工作原理与实战配置
前端·网络·安全
y = xⁿ2 小时前
【LeetCode】哈希表
算法·leetcode·散列表
AI_Claude_code2 小时前
网络基础回顾:DNS、IP封锁与HTTP/S协议关键点
网络·爬虫·python·tcp/ip·http·爬山算法·安全架构
赖134小0747姐2935电2 小时前
罗德与施瓦茨ZN-Z135经济型网络分析仪校准套件26.5G
网络·功能测试·科技·5g
大数据新鸟2 小时前
协议值TCP
服务器·网络·tcp/ip
智者知已应修善业2 小时前
【51单片机独立按键控制数码管动态显示和LED间隔闪烁并清零】2023-5-28
c语言·经验分享·笔记·算法·51单片机
不吃鱼的猫7482 小时前
【音视频流媒体进阶:从网络到 WebRTC】第01篇-Socket 编程基础:TCP 与 UDP 的选择
网络·音视频·webrtc