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 核心架构系统主要由三个核心组件构成Logger (前端/接口层)提供给业务代码调用的接口。它负责初步处理日志级别并将日志信息封装后交给队列。LogQueue (中间缓存层)一个线程安全的循环队列双端队列包装作为生产者业务线程和消费者落盘线程之间的缓冲区。ProcessQueue (后端/落盘层)一个独立的后台线程专门负责从队列中取出消息并执行真正的文件写入操作。2.2 关键技术点异步解耦业务线程通过非阻塞或极低阻塞的方式推送日志。C20 类型安全格式化利用std::format和std::vformat实现类似 Python/Rust 的高效、类型安全的字符串插值。线程同步使用std::mutex保护共享队列利用std::condition_variable实现高效的线程唤醒机制避免死循环消耗 CPU。**优雅关闭 **通过is_shutdown_标志位确保程序退出时队列中剩余的日志能够被完整写入磁盘。可变参数模板引用 C的可变参数模板来实现灵活的日志记录接口可接受任意数量的可转化为字符串的类型。std::forward 原样转发/完美转发 万能引用万能引用接受左值与右值原样转发保证函数内传递参数时参数的类型属性保持不变。3. 实现细节3.1 线程安全的日志队列 (LogQueue)LogQueue封装了std::queuestd::stringpush 操作加锁后入队并调用notify_one()。这只会唤醒一个等待中的后台线程效率高于notify_all()。pop 操作使用unique_lock配合condition_variable::wait()。这种模式能有效处理“虚假唤醒”问题。当系统收到关闭信号且队列为空时返回false引导后台线程退出。3.2 动态格式化与回退机制 (formatMessage)为了增强鲁棒性系统在格式化时做了两层处理首选方案使用std::vformat。它能根据format字符串中的{}占位符一次性注入参数。回退方案 (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. 记录错误信息支持多种类型doubletemperature98.5;logger.log(LogLevel::ERROR,传感器异常! 当前温度: {}, 状态: {},temperature,警告);5.2 注意事项环境要求项目使用了std::format(C20) 和std::thread请确保编译器支持 C20 标准如 GCC 13, Clang 15, MSVC 19.29。性能建议虽然异步日志很快但在极端高频每秒百万级场景下建议根据实际需求调整队列大小或增加缓冲区刷新策略。文件权限请确保程序运行账号对目标日志目录拥有写入权限。6. 总结本项目实现了一个简洁而强大的异步日志工具通过 C 现代特性保证了代码的可读性与安全性。它适用于对延迟敏感、需要记录大量运行时数据的中后台应用系统。用户指定日志级别日志格式以及日志参数让 Logger 合成完整的日志消息并将日志消息输出到用户指定的日志文件中。7.Code#includeiostream#includevector#includemutex#includethread#includestring#includecondition_variable#includesstream#includequeue#includeatomic#includefstream#includeformat#includestring_view//日志级别enumclassLogLevel{INFO,DEBUG,ERROR};classLogQueue{public:LogQueue()default;~LogQueue()default;//msg:格式化日志消息//function:生产线程将格式化消息推入日志队列voidpush(conststd::stringmsg){//操作临界资源时加互斥锁std::lock_guardstd::mutexlock(mtx_);if(!is_shutdown_){//将日志消息推入日志队列中msg_queue_.push(msg);//通知一个消费线程取出消息cond_.notify_one();}}//msg:接受从日志队列取出的日志消息//function消费线程从日志队列中取出日志消息boolpop(std::stringmsg){//操作临界资源时加互斥锁//需要避免虚假唤醒所以采用unique_lockstd::unique_lockstd::mutexlock(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;//此时说明日志队列正在工作并且队列有资源可用弹出资源msgmsg_queue_.front();msg_queue_.pop();returntrue;}//关闭日志队列设置队列状态voidshutdown(){std::lock_guardstd::mutexlock(mtx_);is_shutdown_true;//通知其他所有线程cond_.notify_all();}private://消息队列存储格式化后的消息std::queuestd::stringmsg_queue_;//互斥锁解决多线程互斥问题std::mutex mtx_;//条件变量解决多线程同步问题push/popstd::condition_variable cond_;//日志队列状态默认为开启boolis_shutdown_false;};classLogger{public://filename:日志文件路径//function:构造函数打开日志文件并启动工作线程Logger(conststd::stringfilename):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();}templatetypename...Args//format:日志消息的标签必须为字符串//args:可变参数多个不同类型的消息参数//万能引用即可接受左值也可接受右值//function:将format和args格式化为标准的日志消息字符串//并将格式化日志消息加入日志队列中voidlog(LogLevel level,conststd::stringformat,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_strformatMessage(format,std::forwardArgs(args)...));}private://function:工作线程的工作函数,从日志队列中取出日志消息并将日志消息输出到日志文件中voidprocessQueue(){//接受日志消息std::string msg;//从日志队列中取出日志消息并将日志消息输出到日志文件中while(log_queue_.pop(msg))log_file_msgstd::endl;}//获取当前时间std::stringgetCurrentTime(){autonowstd::chrono::system_clock::now();std::time_t now_timestd::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转变为格式化日志消息 */templatetypename...Argsstd::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::vectorstd::stringarg_strings{std::format({},std::forwardArgs(args))...};std::string result{format};size_t arg_index0;size_t pos0;// 查找并替换 {}while(arg_indexarg_strings.size()(posresult.find({},pos))!std::string::npos){result.replace(pos,2,arg_strings[arg_index]);posarg_strings[arg_index-1].length();}// 补齐剩余参数while(arg_indexarg_strings.size()){resultarg_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_id42;std::string actionlogin;doubleduration3.5;std::string worldWorld;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::exceptionex){std::cerr日志系统初始化失败: ex.what()std::endl;}return0;}
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2602605.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!