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

C++异步日志系统

项目描述

设计并实现一个高效的一部日志打印系统,利用C++的现代特性如可变参数模板、多线程编程(thread)、模板折叠。该系统允许开发者以友好的格式字符串方式记录日志消息,同时保证日志记录过程不会阻塞主线程的执行。

项目目标

1、掌握可变参数模板:理解并应用C++的可变参数模板来实现灵活的日志记录接口

template<template ...Arg>

2、理解多线程与并发:学习如何在C++中创建和管理多个线程,确保线程安全的日志队列实现

thread

3、应用模板折叠:利用模板折叠技术高效地处理参数包,实现占位符格式化日志消息

op,...左折叠右折叠

4、实现异步日志机制:设计一个后台线程持续处理日志队列,异步写入日志文件,避免主线程阻塞

主线程投递队列,子线程消费队列,通过队列实现消息的传递

5、错误处理与资源管理:确保日志系统在异常情况下的稳健性,以及在程序结束时进行优雅的资源释放

项目需求

功能需求

1、日志记录接口

  • 支持带有占位符{}的格式化日志记录,例如log("Hello {}",name);用name替换{}
  • 支持无占位符的日志记录方式,按顺序拼接参数,例如log("Hello ",name);Hello name
  • 支持不同类型的日志参数(整形、浮点型、字符串等)

2、线程安全的日志

  • 实现一个高效的线程安全队列,用于存储写入的日志消息。用互斥锁mutex
  • 支持多生产者(主线程)和单消费者(后台写入线程)的模式。要同步

3、后台写入程序

  • 启动一个独立的后台线程,持续从日志队列中取出日志消息并写入日志文件
  • 确保在程序退出前所有日志消息都被正确写入

4、格式化能力

  • 实现占位符{}的替换逻辑,将参数按顺序填充到日志消息中
  • 处理占位符数量与参数数量不匹配的情况:多余的参数按顺序拼接,缺少参数时保留{}

5、错误处理

  • 捕获并处理可能的异常,如文件打开失败、格式化错误等
  • 保证程序在异常情况下不会崩溃,并提供有意义的错误信息

非功能性要求

性能:

  • 日志记录过程应尽可能高效,避免对主线程造成显著的性能影响。
  • 后台写入线程应能够快速处理日志队列,防止队列过长。

可扩展性:

  • 设计系统时考虑到未来可能的功能扩展,如不同的日志级别(INFO、DEBUG、ERROR)、多目标输出(文件、控制台、网络等)。

可读性与维护性:

  • 代码应遵循清晰的结构和命名规范,便于理解和维护。
  • 提供详细的注释和文档,帮助学生理解每个模块的功能与实现细节。

代码实现

#pragma once
#include<iostream>
#include<queue>
#include<string>
#include<mutex>//锁
#include<condition_variable>//条件变量
#include<thread>//异步打印
#include<fstream>//文件操作
#include<atomic>//原子操作
#include<sstream>//字符串流
#include<vector>
#include<stdexcept>//运行时异常
#include<sstream>
#include<iomanip>
#include<chrono>using namespace std;//辅助函数,将单个参数转换为字符串
template<typename T>
string to_string_helper(T&& arg) {//万能模板引用,不确定为右值还是左值引用ostringstream oss;//字符串输出,类似cout但是保存在内存中oss << forward<T>(arg);//完美转发,根据T的类型,决定是否将arg转发为左值还是右值,保留原始传参的值类别return oss.str();//将ostringstream的内容转为string并返回
}class LogQueue {
public:void push(const string& msg) {//把字符串放到队列//操作队列,一个队列正在操作时其他队列不能进行操作,添加互斥锁lock_guard<mutex> lock(mutex_);//创建锁对象,锁对象会自动加锁,离开作用域会自动解锁queue_.push(msg);//添加数据if (queue_.size() == 1) {cond_var_.notify_one();//唤醒一个等待的线程}}bool pop(string& msg) {//pop  从队列中消费成功返回trueunique_lock<mutex> lock(mutex_);//方法一//先判断队列是否为空//if (queue_.empty()) {//队列为空,为虚假唤醒,则等待//          cond_var_.wait(lock);//}//方法2,lambda表达式:捕获[this]指针,判断队列是否为空//主线程关闭的话也会唤起子线程,所以要判断is_shutdown_设为truecond_var_.wait(lock, [this] {return !queue_.empty() || is_shutdown_; });//如果返回false线程会挂起同时unlock//消费逻辑if (is_shutdown_ && queue_.empty()) {//队列为空,且子线程关闭,则返回falsereturn false;}//while (is_shutdown_ && !queue_.empty()) {//	msg = queue_.front();//	queue_.pop();//	return false;//已经是关闭//}msg = queue_.front();queue_.pop();return true;}void shutdown() {//加锁证明线程唯一lock_guard<mutex> lock(mutex_);is_shutdown_ = true;cond_var_.notify_all();//通知所有消费者要退出}private:queue<string> queue_;//队列mutex mutex_;//线程安全互斥锁condition_variable cond_var_;//线程同步,条件变量bool is_shutdown_ = false;//队列是否关闭};enum class logLevel {INFO,WARNING,ERROR,DEBUG,
};class Logger {
public://绑定filename,后接输出、追加模式Logger(const string& filename) :log_file_(filename, ios::out | ios::app), exit_flag_(false) {//初始化线程,先判断文件流是否打开if (!log_file_.is_open()) {//或者.empty()throw runtime_error("open file error");}//启动线程worker_head_ = thread([this]() {string msg;while (log_queue_.pop(msg)) {//只要队列不为空,就一直从队列中取数据log_file_ << msg << endl;}});}//析构,关闭队列,推出标记true~Logger() {exit_flag_ = true;log_queue_.shutdown();//主线程先等待子线程退出if (worker_head_.joinable()) {worker_head_.join();}//判断文件是否打开,打开就关闭if (log_file_.is_open()) {log_file_.close();}}template<typename... Args>//定义一个可变参数模板void log(logLevel level,const string& format, Args&&... args) {//将格式化字符串和参数转发到日志队列中string level_str;switch (level) {case logLevel::INFO: level_str = "[INFO] "; break;case logLevel::DEBUG: level_str = "[DEBUG] "; break;case logLevel::WARNING: level_str = "[WARNING] "; break;case logLevel::ERROR: level_str = "[ERROR] "; break;}//Args&&是万能引用,可以绑定左值和右值log_queue_.push(level_str + formatMessage(format, forward<Args>(args)...));//只转换了args一个变量,加上...实现展开参数转换//使用forward完美转发,保留原始参数的值类型	}private:string getCurrentTime() {auto now = std::chrono::system_clock::now();auto t = std::chrono::system_clock::to_time_t(now);auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;tm time_info = {};#ifdef _WIN32localtime_s(&time_info, &t);#elselocaltime_r(&t, &time_info);#endifstd::ostringstream ss;ss << std::put_time(&time_info, "%Y-%m-%d %H:%M:%S");return ss.str();}template<typename... Args>string formatMessage(const string& format, Args&&... args) {vector<string> arg_strings={to_string_helper(forward<Args>(args))...};//将可变参数存入到vector//to_string_helper返回一个参数,args是可变参数,所以...重复展开,再用{}传递给to_string_helperostringstream oss;size_t arg_index = 0;//参数索引size_t pos = 0;//匹配位置size_t placeholder = format.find("{}", pos);while (placeholder != string::npos) {//placeholder不等于字符串的无效位置npos就说明找到了oss << format.substr(pos, placeholder - pos);//获取匹配位置之前的字符串if (arg_index < arg_strings.size()) {oss << arg_strings[arg_index++];}else {oss << "{}";//如果匹配位置之后的字符串没有参数,就输出{}}pos = placeholder + 2;//更新找到pos之后的位置placeholder = format.find("{}",pos);//继续匹配}oss << format.substr(pos);//从pos开始一直都没有查找到{},就将整个format转成字符串形式写入到osswhile (arg_index < arg_strings.size()) {//将args_strings的参数写入到ossoss << arg_strings[arg_index++];}return " ["+getCurrentTime()+"]  "+oss.str();}LogQueue log_queue_;thread worker_head_;//工作线程ofstream log_file_;//日志文件atomic<bool> exit_flag_;//退出标志
};

注意事项:

1、防止虚假唤醒,企业写法为方法2

2、析构实现
日志一旦析构,证明子线程也就退出,必须先等待子线程退出!!!

3、模板函数

4、参数插入到模板中
format="Hello {},my name is {},welcome {}"
args_strings={"Tom","Alice"};

先去查找format中的{}位置,再将args_strings参数插入进去

来源:bilibili:【零基础C++(48) 结课实战1-异步日志系统】https://www.bilibili.com/video/BV14HR8YkEkw?vd_source=15c0b606d3052aa65e8da30bd1302034

http://www.xdnf.cn/news/655165.html

相关文章:

  • Keepalived及相关项目
  • 文档工具解析:前端如何选择最适合的文档生成器?
  • PHPStorm运行Thinkphp8.0项目
  • xhr、fetch和axios
  • 无人机降落伞设计要点难点及原理!
  • 基于 uni-app + <movable-view>拖拽实现的标签排序-适用于微信小程序、H5等多端
  • ESP32 LVGL btn事件、label赋值、ddlist选项读取
  • NGINX 用户标识模块 (ngx_http_userid_module) 完整配置与最佳实践指南
  • 知识宇宙-职业篇:嵌入式工程师
  • Pycatia基础代码解析——零件设计篇(一)
  • ATT衰减器(Attenuator)介绍
  • 华为OD机试真题——洞穴探险(2025A卷:200分)Java/python/JavaScript/C/C++/GO最佳实现
  • BGP配置命令详细框架
  • 营销推广需要解决哪些问题?
  • IP SSL证书:为IP地址提供安全加密的专业解决方案
  • 【论文解读】STaR:不用人类思维链指导,模型可以自我进化!
  • Go Web框架选型与实践:基于Gin的REST API开发指南
  • 【R语言科研绘图-最小二乘法】
  • 【混合动力能量管理新突破:负载识别优化策略深度解析与仿真实战】
  • [yolov11改进系列]基于yolov11引入级联群体注意力机制CGAttention的python源码+训练源码
  • 鸿蒙OSUniApp 实现带有滑动删除的列表#三方框架 #Uniapp
  • 基于GitHub Actions+SSH+PM2的Node.js自动化部署全流程指南
  • Nacos集群
  • 【向量数据库选型实战】FAISS vs Chroma vs Milvus vs Qdrant 全面对比
  • 【QT】QString和QStringList去掉空格的方法总结
  • day38python打卡
  • 构建版本没mac上传APP方法
  • 华为OD机试真题——猴子吃桃/爱吃蟠桃的孙悟空(2025B卷:200分)Java/python/JavaScript/C++/C语言/GO六种最佳实现
  • 【C++篇】list模拟实现
  • Qt qml Network error问题