当前位置: 首页 > news >正文

C++异步日志系统

文章目录

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

enumclassLogLevel{INFO,DEBUG,ERROR};

核心方法

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

5. 使用指南

5.1 快速上手

// 1. 创建日志对象Loggerlogger("app.log");// 2. 记录普通信息logger.log(LogLevel::INFO,"服务器启动成功,端口: {}",8080);// 3. 记录错误信息(支持多种类型)doubletemperature=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

#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>//日志级别enumclassLogLevel{INFO,DEBUG,ERROR};classLogQueue{public:LogQueue()=default;~LogQueue()=default;//@msg:格式化日志消息//function:生产线程将格式化消息推入日志队列voidpush(conststd::string&msg){//操作临界资源时加互斥锁std::lock_guard<std::mutex>lock(mtx_);if(!is_shutdown_){//将日志消息推入日志队列中msg_queue_.push(msg);//通知一个消费线程取出消息cond_.notify_one();}}//@msg:接受从日志队列取出的日志消息//function:消费线程从日志队列中取出日志消息boolpop(std::string&msg){//操作临界资源时加互斥锁//需要避免虚假唤醒,所以采用unique_lockstd::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())returnfalse;//此时说明:日志队列正在工作并且队列有资源可用,弹出资源msg=msg_queue_.front();msg_queue_.pop();returntrue;}//关闭日志队列,设置队列状态voidshutdown(){std::lock_guard<std::mutex>lock(mtx_);is_shutdown_=true;//通知其他所有线程cond_.notify_all();}private://消息队列:存储格式化后的消息std::queue<std::string>msg_queue_;//互斥锁:解决多线程互斥问题std::mutex mtx_;//条件变量:解决多线程同步问题:push/popstd::condition_variable cond_;//日志队列状态,默认为开启boolis_shutdown_=false;};classLogger{public://@filename:日志文件路径//function:构造函数,打开日志文件,并启动工作线程Logger(conststd::string&filename):log_file_(filename,std::ios::out|std::ios::app){if(!log_file_.is_open()){throwstd::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格式化为标准的日志消息字符串//并将格式化日志消息加入日志队列中voidlog(LogLevel level,conststd::string&format,Args&&...args){std::string level_str;switch(level){caseLogLevel::INFO:level_str="[INFO] ";break;caseLogLevel::DEBUG:level_str="[DEBUG] ";break;caseLogLevel::ERROR:level_str="[ERROR] ";break;}//std::forward原样转发/完美转发:当接受右值参数时,args为则为右值引用//但右值引用为左值,传入formatMessage时需要保留原本类型log_queue_.push(level_str+formatMessage(format,std::forward<Args>(args)...));}private://function:工作线程的工作函数,从日志队列中取出日志消息,并将日志消息输出到日志文件中voidprocessQueue(){//接受日志消息std::string msg;//从日志队列中取出日志消息,并将日志消息输出到日志文件中while(log_queue_.pop(msg))log_file_<<msg<<std::endl;}//获取当前时间std::stringgetCurrentTime(){autonow=std::chrono::system_clock::now();std::time_t now_time=std::chrono::system_clock::to_time_t(now);charbuffer[100];std::strftime(buffer,sizeof(buffer),"%Y-%m-%d %H:%M:%S",std::localtime(&now_time));returnstd::string(buffer);}/** *@arg:需要转换为字符串的可变参数 *@format:日志消息的标签,必须为字符串 *functin:将format和args转变为格式化日志消息 */template<typename...Args>std::stringformatMessage(std::string_view format,Args&&...args){try{// 使用 vformat 处理运行时的 format 字符串// make_format_args 会类型安全地打包参数return"["+getCurrentTime()+"] "+std::vformat(format,std::make_format_args(args...));}catch(conststd::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_;};intmain(){try{Loggerlogger("log.txt");logger.log(LogLevel::ERROR,"Starting application.");intuser_id=42;std::string action="login";doubleduration=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(conststd::exception&ex){std::cerr<<"日志系统初始化失败: "<<ex.what()<<std::endl;}return0;}

http://www.jsqmd.com/news/793918/

相关文章:

  • Anaconda常用指令集
  • 家政派单小程序源头厂家
  • Ascend NPU高效无损压缩技术解析与优化
  • 启航 —— 二本NPC程序学习之路
  • 从零构建轻量级AI网关:统一管理多模型服务实战指南
  • JavaWeb应用项目开发学习心得:深耕技术体系,践行工程化开发,筑牢企业级Web开发根基
  • 当出海合规压力持续上升时,多云服务容易忽略哪些细节
  • 快图设计:5个理由告诉你为什么这款Vue图片编辑器值得尝试
  • 邮件定时群发系统 - 开源邮件营销平台 | 支持定时发送、联系人管理、数据追踪
  • GPU加速Zak-OTFS调制技术解析与工程实践
  • Java 面向对象-上
  • Error response from daemon: client version 1.52 is too new. Maximum supported API version is 1.43
  • 【测试】之概念篇
  • 小白通俗易懂吃透XXL-JOB:从原理到架构,一篇就够
  • 手把手教你做——助睿实验作业1-订单利润分流数据加工(零代码ETL + 多表关联 + 条件分流),附完整操作步骤
  • Diablo Edit2:暗黑破坏神2角色编辑器完全指南,3步打造完美游戏体验
  • 基于Vue3的一站式AI服务聚合平台开发与部署实战
  • 对比自行搭建代理与使用Taotoken聚合服务在维护精力上的感受
  • txtskills:将llms.txt文档一键转换为AI助手可调用技能
  • 小型嵌入式系统开发流程与实践指南
  • AI驱动材料发现:生成模型、数据集与未来挑战综述
  • 【2026年05月10日】AI编程技术日报 - 每日精选 [特殊字符]
  • 现代JavaScript/TypeScript工具库架构设计与实现指南
  • 带式输送机托辊移动集声故障诊断与多普勒校正【附仿真】
  • 程序员转智能体开发,这8个核心技能,少一个都不行
  • 智能体工程方法论:从AI辅助编码到可控软件开发的范式升级
  • Windows Defender控制权争夺战:开源工具Defender Control的技术解析与实践指南
  • LLMs之Benchmarks:《ProgramBench: Can Language Models Rebuild Programs From Scratch?》翻译与解读
  • 搭建DevOps企业级仿真实验环境:011Kubernetes 核心架构与组件
  • ClawPowers-Agent:基于LLM的智能体框架设计与实战指南