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