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)、多目标输出(文件、控制台、网络等)。

可读性与维护性:

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

代码实现

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

相关推荐
菥菥爱嘻嘻3 小时前
JS手写代码篇---Pomise.race
开发语言·前端·javascript
南瓜胖胖3 小时前
【R语言编程绘图-调色】
开发语言·r语言
lanbing4 小时前
非常适合初学者的Golang教程
开发语言·后端·golang
feiyangqingyun5 小时前
Qt/C++开发监控GB28181系统/sip协议/同时支持udp和tcp模式/底层协议解析
c++·qt·gb28181
stormsha5 小时前
GO语言进阶:掌握进程OS操作与高效编码数据转换
开发语言·数据库·后端·golang·go语言·源代码管理
老神在在0016 小时前
javaEE1
java·开发语言·学习·java-ee
魔道不误砍柴功6 小时前
《接口和抽象类到底怎么选?设计原则与经典误区解析》
java·开发语言
我是李武涯8 小时前
C++ 条件变量虚假唤醒问题的解决
开发语言·c++·算法
编码小笨猪8 小时前
[ Qt ] | 常用控件(三):
开发语言·qt
Bioinfo Guy8 小时前
R包安装报错解决案例系列|R包使用及ARM架构解决data.table安装错误问题
开发语言·arm开发·r语言