项目描述
设计并实现一个高效的一部日志打印系统,利用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)、多目标输出(文件、控制台、网络等)。
可读性与维护性:
- 代码应遵循清晰的结构和命名规范,便于理解和维护。
- 提供详细的注释和文档,帮助学生理解每个模块的功能与实现细节。
代码实现
cpp
#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 从队列中消费成功返回true
unique_lock<mutex> lock(mutex_);
//方法一
//先判断队列是否为空
//if (queue_.empty()) {//队列为空,为虚假唤醒,则等待
// cond_var_.wait(lock);
//}
//方法2,lambda表达式:捕获[this]指针,判断队列是否为空
//主线程关闭的话也会唤起子线程,所以要判断is_shutdown_设为true
cond_var_.wait(lock, [this] {return !queue_.empty() || is_shutdown_; });
//如果返回false线程会挂起同时unlock
//消费逻辑
if (is_shutdown_ && queue_.empty()) {//队列为空,且子线程关闭,则返回false
return 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 _WIN32
localtime_s(&time_info, &t);
#else
localtime_r(&t, &time_info);
#endif
std::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_helper
ostringstream 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转成字符串形式写入到oss
while (arg_index < arg_strings.size()) {//将args_strings的参数写入到oss
oss << 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