C++笔记:流式异步日志库

综合我之前学过的异步日志库,流的缓冲区以及TensorRT里sample的日志设计。总结出了一套流式异步日志。

参考文章:

C++笔记:实现小型日志系统-CSDN博客

TensorRT笔记(2):解析样例中Logger日志类的设计-CSDN博客

C++笔记:std::stringbuf_修改std::string的缓存区-CSDN博客

异步日志

这部分和小型日志系统那块基本一样

cpp 复制代码
//MyLogger.h
#pragma once
#include<thread>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <queue>
#include<iostream>
#include<fstream>
#include<sstream>
#include<chrono>
#include <map>
enum class LogLevel {
	INFO,
	DEBUG,
	WARN,
	ERR,
};
class LogQueue {
public:
	void push(const std::string& msg);
	bool pop(std::string& msg);
	void shutdown();//关闭
private:
	std::queue<std::string> queue;
	std::mutex mtx;
	std::condition_variable cond_var;
	std::atomic<bool> is_shutdown = false;
};
class LogSystem {
public:
	static LogSystem& GetInstance() {
		static LogSystem instance;
		return instance;
	}
	~LogSystem();
	void log(const LogLevel& level, const std::string msg) {
		//加入时间
		auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
		std::tm tm;
		localtime_s(&tm, &now);
		std::ostringstream ts;
		ts << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
		//根据格式化,构造写入的字符串
		std::string result = "[" + ts.str() + "][" + level_string[level] + "] " + msg;
		log_queue.push(result);
	}
private:
	LogSystem();
	LogSystem(const LogSystem&) = delete;
	LogSystem& operator=(const LogSystem&) = delete;
	std::map<LogLevel, std::string> level_string;
	LogQueue log_queue;
	std::thread work_thread;
	std::ofstream log_file;
	std::atomic<bool> exit_flag=false;
	inline static const char* filename = "Log.txt";
};
cpp 复制代码
//MyLogger.cpp
#include "MyLogger.h"
void LogQueue::push(const std::string& msg) {
	{
		std::lock_guard<std::mutex> lock(mtx);
		if (is_shutdown) {
			throw std::runtime_error("LogQueue has been shut down!");
		}
		queue.push(msg);
	}
	cond_var.notify_one();
}

bool LogQueue::pop(std::string& msg)
{
	{
		std::unique_lock<std::mutex> lock(mtx);
		cond_var.wait(lock, [this]() {
			return !queue.empty() || is_shutdown;
			});
		if (is_shutdown && queue.empty()) return false;
		msg = queue.front();
		queue.pop();
	}
	return true;
}

void LogQueue::shutdown()
{
	is_shutdown = true;
	cond_var.notify_all();
}

LogSystem::~LogSystem()
{
	exit_flag = true;
	log_queue.shutdown();
	if (work_thread.joinable()) {
		work_thread.join();
	}
	if (log_file.is_open()) {
		log_file.close();
	}
}

LogSystem::LogSystem() :log_file(filename, std::ios::out | std::ios::app)
{
	if (!log_file.is_open()) {
		throw std::runtime_error("Failed to open log file");
	}
	level_string = { {LogLevel::INFO,"info"},
		{LogLevel::DEBUG,"DEBUG"},
		{LogLevel::WARN,"WARN"},
		{LogLevel::ERR,"ERROR"}
	};
	work_thread = std::thread([this]() {
		std::string msg;
		while (log_queue.pop(msg)) {
			//这里就不打换行了,默认流里面会有
			log_file << msg << std::flush;
		}
		});
}

流式设计

这一块种缓冲区和stringbuf里的示例也基本一样,流的设计参考TensorRT的设计

cpp 复制代码
class LogBuffer :public std::stringbuf {
public:
	explicit LogBuffer(LogLevel level):mLevel(level) {

	}
	~LogBuffer() {
		if (pbase() != pptr()) {
			putOutput();
		}
	}
	int sync()override {
		putOutput();
		return 0;
	}
private:
	LogBuffer(const LogBuffer&) = delete;
	LogBuffer& operator=(const LogBuffer&) = delete;
	void putOutput() {
		//关键,这里丢给异步日志
		LogSystem::GetInstance().log(mLevel, this->str());
		this->str("");
	}
	LogLevel mLevel;
};
class LogStream :public std::ostream {
public:
	explicit LogStream(LogLevel level)
		: std::ostream(nullptr), buf(level) {
		rdbuf(&buf);
	}
private:
	LogBuffer buf;
};
//最后定义了这四个日志等级的宏
#define LOG_INFO  LogStream(LogLevel::INFO)
#define LOG_DEBUG  LogStream(LogLevel::DEBUG)
#define LOG_WARN  LogStream(LogLevel::WARN)
#define LOG_ERR   LogStream(LogLevel::ERR)

rdbuf

cpp 复制代码
// 获取当前 streambuf
std::streambuf* rdbuf() const;

// 设置新的 streambuf,返回旧的
std::streambuf* rdbuf(std::streambuf* sb);

使用测试

cpp 复制代码
int main() {
	auto f = []() {
		LOG_INFO << "hello" << 1 << std::endl;
	};
	std::thread t1(f), t2(f);
	t1.join();
	t2.join();
}
cpp 复制代码
[2025-12-14 16:57:09][info] hello1
[2025-12-14 16:57:09][info] hello1

设计思想

为什么要做成流式?

这个其实没有为什么,用C++笔记:实现小型日志系统-CSDN博客里面的函数接口依然能做到异步日志。做成流只是为了好看。

当然实际上还是方便了一点,不用每次都选择日志等级,而是直接使用对应的宏即可。

并且如果要获取__FILE__,__LINE__等信息,宏因为不会设计函数调用过程,也更精准。

宏的设计

这里比较关键,我们写的这几个宏,对应的日志流对象都是临时的。作用域结束后/或者接收了std::endl等,就会把内容丢到日志队列里去。

为什么不做成全局的日志对象

这是最关键的问题。答案是不能这样做。

如果做成像std::cout那样的全局对象。那么多个线程同时用流的方式往里写,就会出问题。因为都是对同一个流对象写,那就会出现混乱?

对<<操作加锁?就像TensorRT那样。答案也是不行。因为<<加锁只能保证一次<<的原子性。不能保证多线程的顺序性,多线程之间的数据还是会混杂。

如果要对整个流对象加锁,那反而丢失了性能,完全不如临时流对象的效果。临时流对象之间是没有任何约束的,只有在push到日志队列的时候会竞争一下队列的锁。

相关推荐
我叫袁小陌6 小时前
C++内存分布详解
开发语言·c++
Hi202402176 小时前
如何向Virtual Audio Cable写入自定义音频数据
c++·windows·音视频·virtualaudio·虚拟音频线
ht巷子6 小时前
Qt:QPainter坐标系统和坐标转换
开发语言·c++·qt
HalvmånEver6 小时前
Linux:基于匿名管道创建出简易进程池(进程间通信五)
linux·运维·服务器·c++·进程池·管道pipe
郝学胜-神的一滴6 小时前
雕栏玉砌:Qt 自定义窗口之美——标题、圆角、阴影三艺精解
开发语言·c++·qt·程序人生
vyuvyucd6 小时前
C++ SO库创建与调用全攻略
c++
strive-debug6 小时前
cpp基础入门~~c语言的补足
开发语言·c++
_Kayo_6 小时前
node.js 学习笔记5
笔记·学习
乌萨奇也要立志学C++6 小时前
【洛谷】离散化专题 模板精讲 + 火烧赤壁 & 贴海报实战
数据结构·c++·算法
小龙报6 小时前
【算法通关指南:数据结构与算法篇 】二叉树相关算法题:1.新二叉树 2.二叉树的遍历
c语言·数据结构·c++·人工智能·物联网·算法·深度优先