C++异步日志系统

文章目录

  • 异步日志系统
    • [1. 项目背景](#1. 项目背景)
    • [2. 设计思路](#2. 设计思路)
      • [2.1 核心架构](#2.1 核心架构)
      • [2.2 关键技术点](#2.2 关键技术点)
    • [3. 实现细节](#3. 实现细节)
      • [3.1 线程安全的日志队列 (LogQueue)](#3.1 线程安全的日志队列 (LogQueue))
      • [3.2 动态格式化与回退机制 (formatMessage)](#3.2 动态格式化与回退机制 (formatMessage))
      • [3.3 自动化管理](#3.3 自动化管理)
    • [4. 接口说明](#4. 接口说明)
    • [5. 使用指南](#5. 使用指南)
      • [5.1 快速上手](#5.1 快速上手)
      • [5.2 注意事项](#5.2 注意事项)
    • [6. 总结](#6. 总结)
  • 7.Code

异步日志系统

1. 项目背景

在高性能服务器或复杂桌面应用开发中,日志系统是不可或缺的调试与监控工具。然而,传统的同步日志(直接写入文件)存在明显的性能瓶颈:

  • 磁盘 I/O 阻塞:由于磁盘写入速度远慢于 CPU 处理速度,主逻辑线程(业务线程)常因等待 I/O 完成而挂起。
  • 资源竞争:多线程环境下,频繁的文件加锁会导致严重的性能下降。

本项目实现了一个基于生产者-消费者模型的异步日志系统,将日志的格式化处理与磁盘写入解耦,最大程度降低日志记录对业务逻辑的影响。

2. 设计思路

2.1 核心架构

系统主要由三个核心组件构成:

  1. Logger (前端/接口层):提供给业务代码调用的接口。它负责初步处理日志级别,并将日志信息封装后交给队列。
  2. LogQueue (中间缓存层):一个线程安全的循环队列(双端队列包装),作为生产者(业务线程)和消费者(落盘线程)之间的缓冲区。
  3. ProcessQueue (后端/落盘层):一个独立的后台线程,专门负责从队列中取出消息并执行真正的文件写入操作。

2.2 关键技术点

  • 异步解耦:业务线程通过非阻塞(或极低阻塞)的方式推送日志。
  • C++20 类型安全格式化 :利用 std::formatstd::vformat 实现类似 Python/Rust 的高效、类型安全的字符串插值。
  • 线程同步 :使用 std::mutex 保护共享队列,利用 std::condition_variable 实现高效的线程唤醒机制,避免死循环消耗 CPU。
  • **优雅关闭 **:通过 is_shutdown_ 标志位确保程序退出时,队列中剩余的日志能够被完整写入磁盘。
  • 可变参数模板:引用 C++的可变参数模板来实现灵活的日志记录接口,可接受任意数量的可转化为字符串的类型。
  • std::forward 原样转发/完美转发 万能引用:万能引用接受左值与右值,原样转发保证函数内传递参数时参数的类型属性保持不变。

3. 实现细节

3.1 线程安全的日志队列 (LogQueue)

LogQueue 封装了 std::queue<std::string>

  • push 操作 :加锁后入队,并调用 notify_one()。这只会唤醒一个等待中的后台线程,效率高于 notify_all()
  • pop 操作 :使用 unique_lock 配合 condition_variable::wait()。这种模式能有效处理"虚假唤醒"问题。当系统收到关闭信号且队列为空时,返回 false 引导后台线程退出。

3.2 动态格式化与回退机制 (formatMessage)

为了增强鲁棒性,系统在格式化时做了两层处理:

  1. 首选方案 :使用 std::vformat。它能根据 format 字符串中的 {} 占位符一次性注入参数。
  2. 回退方案 (Fallback) :如果用户提供的占位符数量与参数不匹配(触发 format_error),系统会自动将所有参数转为字符串,并手动尝试替换 {},剩余多出的参数将直接拼接在末尾,确保信息不丢失。

3.3 自动化管理

  • RAII 模式Logger 的构造函数负责初始化资源(打开文件、启动线程),析构函数负责回收资源(停止队列、汇合线程、关闭文件),无需手动干预生命周期。

4. 接口说明

日志级别 (LogLevel)

cpp 复制代码
enum class LogLevel { INFO, DEBUG, ERROR };

核心方法

  • **Logger(const std::string& filename)**: 构造函数,指定日志输出路径。
  • **void log(LogLevel level, const std::string& format, Args&&... args)**:
    • level: 日志级别,日志重要程度。
    • format: 包含 {} 的格式化字符串。
    • args: 可变参数模板,支持任意可被格式化的类型。

5. 使用指南

5.1 快速上手

cpp 复制代码
// 1. 创建日志对象
Logger logger("app.log");

// 2. 记录普通信息
logger.log(LogLevel::INFO, "服务器启动成功,端口: {}", 8080);

// 3. 记录错误信息(支持多种类型)
double temperature = 98.5;
logger.log(LogLevel::ERROR, "传感器异常! 当前温度: {}, 状态: {}", temperature, "警告");

5.2 注意事项

  1. 环境要求 :项目使用了 std::format (C++20) 和 std::thread,请确保编译器支持 C++20 标准(如 GCC 13+, Clang 15+, MSVC 19.29+)。
  2. 性能建议:虽然异步日志很快,但在极端高频(每秒百万级)场景下,建议根据实际需求调整队列大小或增加缓冲区刷新策略。
  3. 文件权限:请确保程序运行账号对目标日志目录拥有写入权限。

6. 总结

本项目实现了一个简洁而强大的异步日志工具,通过 C++ 现代特性保证了代码的可读性与安全性。它适用于对延迟敏感、需要记录大量运行时数据的中后台应用系统。

用户指定日志级别,日志格式以及日志参数让 Logger 合成完整的日志消息,并将日志消息输出到用户指定的日志文件中。

7.Code

cpp 复制代码
#include<iostream>
#include<vector>
#include<mutex>
#include<thread>
#include<string>
#include<condition_variable>
#include<sstream> 
#include<queue>
#include<atomic>
#include<fstream>
#include<format>
#include <string_view>

//日志级别
enum class LogLevel { INFO, DEBUG, ERROR };

class LogQueue{
public:
    LogQueue()=default;
    ~LogQueue()=default;

    //@msg:格式化日志消息
    //function:生产线程将格式化消息推入日志队列
    void push(const std::string&msg){
        //操作临界资源时加互斥锁
        std::lock_guard<std::mutex>lock(mtx_);
        if(!is_shutdown_){
            //将日志消息推入日志队列中
            msg_queue_.push(msg);
            //通知一个消费线程取出消息
            cond_.notify_one();
        }
    }

    //@msg:接受从日志队列取出的日志消息
    //function:消费线程从日志队列中取出日志消息
    bool pop(std::string&msg){
        //操作临界资源时加互斥锁
        //需要避免虚假唤醒,所以采用unique_lock
        std::unique_lock<std::mutex>lock(mtx_);
        //避免虚假唤醒:线程被唤醒但是没有资源可用
        /*比如:
        1.队列为空。线程 A 进入 wait。
        2.生产者往队列放了一个数据,并调用了 notify_one()。
        3.此时,另一个正好在运行的线程 B(可能没在 wait,只是刚准备 pop)抢先获取了互斥锁,并把刚放进去的数据取走了。
        4.线程 A 终于醒来并拿到了锁,但此时队列又是空的了。
        结果:对于线程 A 来说,这次唤醒就是"虚假"的,因为它醒来后发现没活干*/
        while(msg_queue_.empty()&&!is_shutdown_)cond_.wait(lock);
        //当shutdown通知所有线程时,说明队列关闭,取出资源失败
        if(is_shutdown_&&msg_queue_.empty())return false;
        //此时说明:日志队列正在工作并且队列有资源可用,弹出资源
        msg=msg_queue_.front();
        msg_queue_.pop();
        return true;
    }
    
    //关闭日志队列,设置队列状态
    void shutdown(){
        std::lock_guard<std::mutex>lock(mtx_);
        is_shutdown_=true;
        //通知其他所有线程
        cond_.notify_all();
    }

private:
    //消息队列:存储格式化后的消息
    std::queue<std::string>msg_queue_;
    //互斥锁:解决多线程互斥问题
    std::mutex mtx_;
    //条件变量:解决多线程同步问题:push/pop
    std::condition_variable cond_;
    //日志队列状态,默认为开启
    bool is_shutdown_=false;
};

class Logger{
public:
    //@filename:日志文件路径
    //function:构造函数,打开日志文件,并启动工作线程
    Logger(const std::string&filename):log_file_(filename,std::ios::out|std::ios::app){
        if(!log_file_.is_open()){
            throw std::runtime_error("无法打开日志文件");
        }
        //启动工作线程,传入工作函数
        work_thread_=std::thread(processQueue,this);
    }

    //function:析构函数,关闭日志队列,日志文件以及回收工作线程
    ~Logger(){
        //关闭日志队列
        log_queue_.shutdown();
        //关闭日志文件
        if(log_file_.is_open())log_file_.close();
        //回收工作线程
        if(work_thread_.joinable())work_thread_.join();
    }

    template<typename... Args>
    //@format:日志消息的标签,必须为字符串
    //@args:可变参数,多个不同类型的消息参数
    //万能引用,即可接受左值也可接受右值
    //function:将format和args格式化为标准的日志消息字符串
    //并将格式化日志消息加入日志队列中
    void log(LogLevel level,const std::string&format,Args&&... args){
        std::string level_str;
        switch(level) {
            case LogLevel::INFO: level_str = "[INFO] "; break;
            case LogLevel::DEBUG: level_str = "[DEBUG] "; break;
            case LogLevel::ERROR: level_str = "[ERROR] "; break;
        }
        //std::forward原样转发/完美转发:当接受右值参数时,args为则为右值引用
        //但右值引用为左值,传入formatMessage时需要保留原本类型
        log_queue_.push(level_str+formatMessage(format,std::forward<Args>(args)...));
    }

private:
    //function:工作线程的工作函数,从日志队列中取出日志消息,并将日志消息输出到日志文件中
    void processQueue(){
        //接受日志消息
        std::string msg;
        //从日志队列中取出日志消息,并将日志消息输出到日志文件中
        while(log_queue_.pop(msg))log_file_<<msg<<std::endl;
    }

    //获取当前时间
    std::string getCurrentTime() {
        auto now = std::chrono::system_clock::now();
        std::time_t now_time = std::chrono::system_clock::to_time_t(now);
        char buffer[100];
        std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", std::localtime(&now_time));
        return std::string(buffer);
    }

    /** 
    *@arg:需要转换为字符串的可变参数
    *@format:日志消息的标签,必须为字符串
    *functin:将format和args转变为格式化日志消息
    */
    template<typename... Args>
    std::string formatMessage(std::string_view format, Args&&... args) {
        try {
            // 使用 vformat 处理运行时的 format 字符串
            // make_format_args 会类型安全地打包参数
            return "[" + getCurrentTime() + "] " +std::vformat(format, std::make_format_args(args...));
        }
        catch (const std::format_error&) {
            // 如果失败(通常是占位符数量不匹配),执行回退
            // 这里我们可以利用 std::format 本身来简化单个参数的转换
            std::vector<std::string> arg_strings = { 
                std::format("{}", std::forward<Args>(args))... 
            };
            
            std::string result { format };
            size_t arg_index = 0;
            size_t pos = 0;

            // 查找并替换 {}
            while (arg_index < arg_strings.size() && 
                (pos = result.find("{}", pos)) != std::string::npos) {
                result.replace(pos, 2, arg_strings[arg_index++]);
                pos += arg_strings[arg_index - 1].length();
            }

            // 补齐剩余参数
            while (arg_index < arg_strings.size()) {
                result += arg_strings[arg_index++];
            }

            return "[" + getCurrentTime() + "] " +result;
        }
    }

    //日志队列:存取消息的容器
    LogQueue log_queue_;
    //工作/消费线程:负责将日志队列中的消息输出到日志文件中
    std::thread work_thread_;
    //日志文件:存储程序写入的日志消息,供客户查看
    std::ofstream log_file_;
};

int main() {
    try {
        Logger logger("log.txt");

        logger.log(LogLevel::ERROR,"Starting application.");

        int user_id = 42;
        std::string action = "login";
        double duration = 3.5;
        std::string world = "World";

        logger.log(LogLevel::INFO,"User {} performed {} in {} seconds.", user_id, action, duration);
        logger.log(LogLevel::ERROR,"Hello {}", world);
        logger.log(LogLevel::DEBUG,"This is a message without placeholders.");
        logger.log(LogLevel::ERROR,"Multiple placeholders: {}, {}, {}.", 1, 2, 3);

        // 模拟一些延迟以确保后台线程处理完日志
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    catch (const std::exception& ex) {
        std::cerr << "日志系统初始化失败: " << ex.what() << std::endl;
    }

    return 0;
}
相关推荐
xyq20241 小时前
SVN 提交操作详解
开发语言
kyle~1 小时前
ROS2---路径机制辨析
c++·机器人·ros2
Halo_tjn1 小时前
基于异常处理机制 相关知识点
java·开发语言·算法
沐知全栈开发1 小时前
WebPages 对象
开发语言
谙弆悕博士1 小时前
Lua学习笔记
c语言·开发语言·笔记·学习·lua·创业创新·业界资讯
Data_Journal1 小时前
2026年十大数据集网站
大数据·开发语言·数据库·人工智能·python
cui_ruicheng1 小时前
Linux线程(三):线程同步、互斥与生产者消费者模型
linux·服务器·开发语言
CryptoPP2 小时前
解锁股票数据可视化新姿势:轻量级数据接口与动态图表实践
大数据·开发语言·人工智能·信息可视化·金融·区块链