日志系统7--异步日志的实现

整体设计思路

异步日志器的核心目标是解耦业务线程的日志写入和磁盘 IO 操作,避免业务线程被慢速的磁盘 IO 阻塞。整体流程:

核心特点:

  1. 前端(业务线程)只做内存操作(appendcurrentBuffer_),速度极快;

  2. 后台线程单独处理磁盘 IO,支持 "定时刷盘(flushInterval_秒)" 和 "缓冲区满触发刷盘";

  3. 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();
    }

核心亮点

  1. std::move(currentBuffer_):string 的 move 语义避免了大字符串的拷贝(4000 字节的拷贝会消耗性能),仅转移字符串的内部指针;

  2. 锁的粒度:仅在 "判断缓冲区 + 写入缓冲区" 时加锁,锁粒度极小,对业务线程性能影响可忽略;

  3. 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 == nullptrlen <= 0,可能导致崩溃;
  • basename_初始化缺失:原代码构造函数中未初始化basename_(已在之前的修复中补充),会导致 LogFile 创建失败;
  • 日志格式:当前仅写入原始日志内容,缺少时间、级别、文件 / 行号等上下文(需结合 Logger 类补充);
  • 异常处理:未处理 LogFile 写磁盘失败的情况(如磁盘满、权限不足)。

总结

  1. 核心架构:前端内存缓冲区 + 后台线程批量刷盘,彻底解耦业务线程和磁盘 IO;
  2. 性能核心:最小化锁粒度、批量 IO、move 语义、预分配空间,保证高并发下的低延迟;
  3. 可靠性保障:过载保护(丢弃多余日志)、析构刷盘、停止线程时的兜底处理,避免日志丢失;
  4. 适用场景:高并发的后端服务(如服务器、中间件),需要高性能、低阻塞的日志输出。

整体代码讲解

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();
    }
}
相关推荐
程序员Jared2 小时前
C++11—this_thread
c++·this_thread
mjhcsp2 小时前
C++ Manacher 算法:原理、实现与应用全解析
java·c++·算法·manacher 算法
Z1Jxxx2 小时前
0和1的个数
数据结构·c++·算法
朔北之忘 Clancy3 小时前
2020 年 6 月青少年软编等考 C 语言二级真题解析
c语言·开发语言·c++·学习·青少年编程·题解·尺取法
消失的旧时光-19433 小时前
C++ 中的 auto 与 nullptr:不是语法糖,而是类型系统升级
开发语言·c++
fpcc3 小时前
跟我学C++中级篇—C++17中的元编程逻辑操作
c++·模板编程
HABuo3 小时前
【Linux进程(五)】进程地址空间深入剖析-->虚拟地址、物理地址、逻辑地址的区分
linux·运维·服务器·c语言·c++·后端·centos
AuroraWanderll3 小时前
类和对象(六)--友元、内部类与再次理解类和对象
c语言·数据结构·c++·算法·stl
Tim_103 小时前
【C++入门】05、复合类型-数组
开发语言·c++·算法