[linux仓库]告别空洞理论!手写一个高性能日志模块,为线程池实战铺路[线程·捌]

[linux仓库]告别空洞理论!手写一个高性能日志模块,为线程池实战铺路[线程·捌]


🌟 各位看官好,我是!

🌍 Linux == Linux is not Unix !


🚀 为了给线程池做铺垫,需要手写日志模块方便调试,并引入所谓的池化技术以及日志所采用的策略模式。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享更多人哦!

目录

书接上文

谈兵先识器:在构建线程池之前,我们先聊聊“池化技术”

什么是池化技术?—— 一种“未雨绸缪”的智慧

为什么要池化?—— 因为“从零创建”的代价远比你想象的要高

日志与策略模式

日志认识

代码编写

日志等级

时间戳

文件名和行号

刷新策略

激活策略

日志内容

总结

附源码


书接上文

谈兵先识器:在构建线程池之前,我们先聊聊“池化技术”

书接上文,在我们掌握了线程控制、锁以及条件变量这些并发编程的利器之后,我们终于可以着手设计并实现一个真正实用的组件——线程池。

但在我们敲下第一行代码之前,有一个更宏观、更具指导性的思想需要先深入理解,那就是“池化技术”(Pooling Technology)。这不仅是线程池的核心,也是构建几乎所有高性能后台服务的基石。

什么是池化技术?—— 一种“未雨绸缪”的智慧

让我们先抛开代码,来看一个生活中的例子:

想象一下,你经营着一家非常火爆的网约车公司。每当有乘客下单时,你才开始打电话招募司机、给他们注册、分配车辆。等这一套流程走完,乘客早已不耐烦地取消了订单。

聪明的做法是什么?你提前招募并培训好一批司机,让他们在几个热门地段的“司机站”里随时待命。订单一来,你立刻从站里派一位空闲的司机出发。任务完成后,司机不是解雇回家,而是返回站点继续等待下一个订单。

这个“司机站”就是“”。池化技术的核心思想就是:将一批昂贵的、需要频繁使用的资源预先创建好并统一管理起来,当需要时直接从“池”中获取,用完后不是销毁,而是归还给“池”,以供后续复用。

为什么要池化?—— 因为“从零创建”的代价远比你想象的要高

正如下图提到的,池化技术旨在“减少底层重复工作”。

然而,如果我们直接一头扎进复杂的并发代码中,就好比在没有地图和手电筒的情况下探索一个漆黑的洞穴——我们很快就会迷失方向。

这个“手电筒”和“地图”,就是日志系统。在多线程环境中,断点调试(GDB)的作用会因为线程间的时序和调度问题而大打折扣。一个可靠的、能够记录关键信息和错误状态的日志系统,是我们分析、调试和监控我们未来线程池运行状态的生命线。

但在动手写日志系统之前,我们先要学习一种能让它变得无比灵活和强大的设计模式——策略模式 (Strategy Pattern)

日志与策略模式

为了讲解日志模式,这里不得不提到设计模式,那么什么是设计模式呢?

IT⾏业这么⽕, 涌⼊的⼈很多. 俗话说林⼦⼤了啥⻦都有. ⼤佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖⼤佬的后腿, 于是⼤佬们针对⼀些经典的常⻅的场景, 给定了⼀些对应的解决⽅案,这个就是设计模式.(可是我怎么从来没有看到啊?在腾讯、阿里、谷歌等大厂中公布了一些开源的日志设计,等会在编写过程中会使用到谷歌是如何搭建日志框架,如何进行设计的. )

日志认识

计算机中的⽇志是记录系统和软件运⾏中发⽣事件的⽂件,主要作⽤是监控运⾏状态、记录异常信息,帮助快速定位问题并⽀持程序员进⾏问题修复。它是系统维护、故障排查和安全管理的重要工具。

那么一个日志有哪些必要的指标呢?格式应该是怎样的呢?

必要:

  1. 时间戳(记录出现问题时的时间)
  2. 日志内容(我报的内容是啥错?)
  3. 日志等级(这个问题是正常输出的;是申请失败,不影响运行;还是引起错误,可以重新重启的呢?)
  • DEBUG,做测试
  • INFO,正常输出
  • WARING:申请失败,不影响运行
  • ERROR:引起错误,可以重新重启
  • FATAL:不一定是正常结束,需要排查

可选:

  1. 文件名和行号
  2. 进程,线程相关id信息等

日志格式:

[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容,⽀持可变参数 [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [17] - hello world [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [21] - hello world [2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world

代码编写

进行日志格式代码设计的时候,需要注意以下两个核心问题:

  1. 刷新策略 --> 是向显示器刷新,还是文件刷新,还是网络等等呢?
  2. 需要刷新那么就一定得构建一条完整的日志,而这需要日志格式的支持.

日志等级

// 规定出场景的日志等级 enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL }; std::string Level2String(LogLevel level) { switch (level) { case LogLevel::DEBUG: return "Debug"; case LogLevel::INFO: return "Info"; case LogLevel::WARNING: return "Warning"; case LogLevel::ERROR: return "Error"; case LogLevel::FATAL: return "Fatal"; default: return "Unknown"; } }

这里通过枚举分为5个日志等级,那为什么还需要Level2String这个函数呢?

DEBUG 等在编译后就是数字,为了将枚举值转换为人类可读的字符串,因此用Level2String函数将数字转为字符串.

时间戳

系统提供一个获取时间戳的系统调用:time_t time(time_t *_Nullable tloc);tloc指的是时区,默认为nullptr即可.time_t是什么呢? long int 一个长整数
 // 1. 获取时间戳 time_t currtime = time(nullptr);

可是时间戳我们看不懂啊?同样需要转为人类看的懂的时间,提供了一套函数把时间戳转换为人类看的懂的语言

struct tm *localtime(const time_t *timep);
struct tm *localtime_r(const time_t *restrict timep, struct tm *restrict result);timep 是输入参数;restrict result 是输出参数.

这两个函数都将一个指向 time_t 类型(通常是自 1970-01-01 00:00:00 +0000 (UTC) 以来的秒数)的指针作为输入,并将其转换为本地时间(根据系统设定的时区进行计算),然后将转换后的结果填充到一个 struct tm 结构体中。

带_r是什么意思呢?凡是带_r的代表该函数是可被重入的.
struct tm { int tm_sec; /* Seconds [0, 60] */ int tm_min; /* Minutes [0, 59] */ int tm_hour; /* Hour [0, 23] */ int tm_mday; /* Day of the month [1, 31] */ int tm_mon; /* Month [0, 11] (January = 0) */ int tm_year; /* Year minus 1900 */ int tm_wday; /* Day of the week [0, 6] (Sunday = 0) */ int tm_yday; /* Day of the year [0, 365] (Jan/01 = 0) */ int tm_isdst; /* Daylight savings flag */ long tm_gmtoff; /* Seconds East of UTC */ const char *tm_zone; /* Timezone abbreviation */ }; 
 // 2. 如何把时间戳转换成为20XX-08-04 12:27:03 struct tm currtm; localtime_r(&currtime, &currtm); // 3. 转换成为字符串 -- dubug? char timebuffer[64]; snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d", currtm.tm_year + 1900, currtm.tm_mon + 1, currtm.tm_mday, currtm.tm_hour, currtm.tm_min, currtm.tm_sec);

文件名和行号

#define LOG(level) logger(level, __FILE__, __LINE__)__FILE__:自动替换为当前源代码文件的文件名(字符串类型);__LINE__:自动替换为当前代码所在的行号(整数类型)。

刷新策略

假设这时已经拿到了一条完整的日志,而这条日志是向显示器刷新还是向文件刷新还是网络刷新?由我们进行选择,因此我们必须要写一个关于刷新策略的类.这里通过定义:

  • LogStrategy的基类 -- 定义统一的日志行为,定义所有日志输出策略必须实现的核心方法,确保不同输出方式的接口一致性。
// 1. 刷新的问题 -- 假设我们已经有了一条完整的日志,string->设备(显示器,文件) // 基类方法 class LogStrategy { public: virtual ~LogStrategy() = default; virtual void SyncLog(const std::string &logmessage) = 0; };
  • ConsoleLogStrategy—— 控制台日志输出

将来我们的日志可以被多种线程访问的,如果 刷新测量是往显示器做刷新,一旦刷新往显示器上打,显示器作为临界资源就需要被保护起来,因此需要进行加锁啊!

// 显示器刷新 class ConsoleLogStrategy : public LogStrategy { public: ~ConsoleLogStrategy() { } void SyncLog(const std::string &logmessage) override { { LockGuard lockguard(&_lock); std::cout << logmessage << std::endl; } } private: Mutex _lock; };
  • FileLogStrategy—— 文件日志输出

当刷新策略设定为向文件刷新日志时,自然会想到对日志信息按等级分类 —— 比如哪些属于 INFO 级、哪些是 ERROR 级、哪些又该归为 FATAL 级,再将不同等级的日志分别写入对应文件中。但随之会遇到一个问题:若把这些日志文件都直接放在当前目录下,随着时间推移,文件会越积越多,整个目录会显得杂乱无章。所以很自然地会考虑创建一个专门的 log 目录,用来统一存放这些日志文件。可这里又有了新的疑问:我们目前只学过用 mkdir 指令来创建目录,却还没接触过通过系统调用来创建目录的方法,那该如何通过系统调用实现 log 目录的创建呢?

int mkdir(const char *pathname, mode_t mode);

这里并不用上面的系统调用,提供C++17的文件操作 --> 判断目录是否存在,不存在就创建.

std::filesystem::exists(_dir_path_name); //检查指定路径对应的目录或文件是否存在 std::filesystem::create_directories(_dir_path_name); //用于递归创建目录
const std::string logdefaultdir = "log"; const static std::string logfilename = "test.log"; // 文件刷新 class FileLogStrategy : public LogStrategy { public: FileLogStrategy(const std::string &dir = logdefaultdir, const std::string filename = logfilename) : _dir_path_name(dir), _filename(filename) { LockGuard lockguard(&_lock); if (std::filesystem::exists(_dir_path_name)) { return; } // 可能父目录不允许创建等问题,所以需要try一下 try { std::filesystem::create_directories(_dir_path_name); } catch (const std::filesystem::filesystem_error &e) { std::cerr << e.what() << "\r\n"; } } void SyncLog(const std::string &logmessage) override { { LockGuard lockguard(&_lock); std::string target = _dir_path_name; target += "/"; target += _filename; std::ofstream out(target.c_str(), std::ios::app); // append if (!out.is_open()) { return; } out << logmessage << "\n"; // out.write out.close(); } } ~FileLogStrategy() { } private: std::string _dir_path_name; // log std::string _filename; // hello.log => log/hello.log Mutex _lock; }; // 网络刷新
激活策略
class Logger { public: Logger() { } void EnableConsoleLogStrategy() { _strategy = std::make_unique<ConsoleLogStrategy>(); } void EnableFileLogStrategy() { _strategy = std::make_unique<FileLogStrategy>(); } ~Logger() { } private: std::unique_ptr<LogStrategy> _strategy; };

日志内容

实现日志信息的方法有很多种,这里介绍一种谷歌实现日志的十分巧妙的方法:

首先定义了一个内部类Logger,为了能支持将一条日志信息刷新出去,将外部类的刷新策略也带进内部类里,这样一旦形成日志信息,就可以把数据刷新出去.

 class LogMessage { private: std::string _curr_time; // 日志时间 LogLevel _level; // 日志等级 pid_t _pid; // 进程pid std::string _filename; int _line; std::string _loginfo; // 一条合并完成的,完整的日志信息 Logger &_logger; // 提供刷新策略的具体做法 };
 LOG(LogLevel::ERROR) << "hello world" << ", 3.14 " << 123;

LogMessage的构造如下:

 LogMessage(LogLevel level, std::string &filename, int line, Logger &logger) : _curr_time(GetCurrentTime()), _level(level), _pid(getpid()), _filename(filename), _line(line), _logger(logger) { std::stringstream ss; ss << "[" << _curr_time << "] " << "[" << Level2String(_level) << "] " << "[" << _pid << "] " << "[" << _filename << "] " << "[" << _line << "]" << " - "; _loginfo = ss.str(); } 

需要注意的是日志内容会面临参数不一定,类型也不一定的问题啊!那该怎样把一条完整的内容拿到呢?这里在内部类对<<做重载

 template<typename T> LogMessage& operator << (const T &info) { std::stringstream ss; ss << info; _loginfo += ss.str(); return *this; }

可是如何才能调用内部类的构造和<<运算符重载呢?这无疑成为了一个难点:

谷歌采用一个非常精妙的方法:在外部类定义一个()的运算符重载

 LogMessage operator()(LogLevel level, std::string filename, int line) { return LogMessage(level, filename, line, *this); } 

是啊总算可以串起来了:

LOG(LogLevel::ERROR) << "hello world" << ", 3.14 " << 123;

会被宏替换为logger(LogLevel::ERROR, __FILE__, __LINE__) << "hello world" << ", 3.14 " << 123;

接着logger(LogLevel::ERROR, __FILE__, __LINE__)会调用()运算符重载,故意拷贝,形成LogMessage临时对象,拿到了除日志内容的相关信息,需要注意的是()重载返回的是一个临时对象啊 ! 后续在被<<时,会被持续引⽤,直到没有<<,_loginfo已经有一条完整的信息了.此时还差一口气没有补上,如何把这条日志信息刷新出去呢?由于该临时对象没被<<时,生命周期结束了,此时会被析构啊!那么我们在析构函数这将信息刷新出去不就行了?

总结

本文介绍了Linux日志系统的设计与实现,重点讲解了日志格式设计和刷新策略。主要内容包括:

  1. 日志等级划分(DEBUG/INFO/WARNING/ERROR/FATAL)及对应字符串转换;
  2. 时间戳获取与格式化;
  3. 基于策略模式的日志输出方式(控制台输出、文件输出等);
  4. 采用Google风格的日志内容构建方法,通过运算符重载实现灵活的日志信息拼接。文章还详细阐述了文件日志策略中的目录创建、线程安全处理等关键技术点,最终实现了一个支持多级别、多输出方式的灵活日志系统框架。

附源码

#pragma once #include <iostream> #include <string> #include <filesystem> // C++17 文件操作 #include <fstream> #include <ctime> #include <unistd.h> #include <memory> #include <sstream> #include "Mutex.hpp" // 1.将一条日志信息进行分等级 // 2.获得时间戳,转换为年月日 // 3.选择刷新策略 // 4.把消息带出去 // 规定出场景的日志等级 enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL }; std::string Level2String(LogLevel level) { switch (level) { case LogLevel::DEBUG: return "Debug"; case LogLevel::INFO: return "Info"; case LogLevel::WARNING: return "Warning"; case LogLevel::ERROR: return "Error"; case LogLevel::FATAL: return "Fatal"; default: return "Unknown"; } } // 20XX-08-04 12:27:03 std::string GetCurrentTime() { // 1. 获取时间戳 time_t currtime = time(nullptr); // 2. 如何把时间戳转换成为20XX-08-04 12:27:03 struct tm currtm; localtime_r(&currtime, &currtm); // 3. 转换成为字符串 -- dubug? char timebuffer[64]; snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d", currtm.tm_year + 1900, currtm.tm_mon + 1, currtm.tm_mday, currtm.tm_hour, currtm.tm_min, currtm.tm_sec); return timebuffer; } /////////////////////////////////////////////////////////////////// // 1. 刷新的问题 -- 假设我们已经有了一条完整的日志,string->设备(显示器,文件) // 基类方法 class LogStrategy { public: virtual ~LogStrategy() = default; virtual void SyncLog(const std::string &logmessage) = 0; }; // 显示器刷新 class ConsoleLogStrategy : public LogStrategy { public: ~ConsoleLogStrategy() { } void SyncLog(const std::string &logmessage) override { { LockGuard lockguard(&_lock); std::cout << logmessage << std::endl; } } private: Mutex _lock; }; const std::string logdefaultdir = "log"; const static std::string logfilename = "test.log"; // 文件刷新 class FileLogStrategy : public LogStrategy { public: FileLogStrategy(const std::string &dir = logdefaultdir, const std::string filename = logfilename) : _dir_path_name(dir), _filename(filename) { LockGuard lockguard(&_lock); if (std::filesystem::exists(_dir_path_name)) { return; } try { std::filesystem::create_directories(_dir_path_name); } catch (const std::filesystem::filesystem_error &e) { std::cerr << e.what() << "\r\n"; } } void SyncLog(const std::string &logmessage) override { { LockGuard lockguard(&_lock); std::string target = _dir_path_name; target += "/"; target += _filename; std::ofstream out(target.c_str(), std::ios::app); // append if (!out.is_open()) { return; } out << logmessage << "\n"; // out.write out.close(); } } ~FileLogStrategy() { } private: std::string _dir_path_name; // log std::string _filename; // hello.log => log/hello.log Mutex _lock; }; // 网络刷新 //////////////////////////////////////////////////////// // 1. 定制刷新策略 // 2. 构建完整的日志 class Logger { public: Logger() { } void EnableConsoleLogStrategy() { _strategy = std::make_unique<ConsoleLogStrategy>(); } void EnableFileLogStrategy() { _strategy = std::make_unique<FileLogStrategy>(); } // 形成一条完整日志的方式 class LogMessage { public: LogMessage(LogLevel level, std::string &filename, int line, Logger &logger) : _curr_time(GetCurrentTime()), _level(level), _pid(getpid()), _filename(filename), _line(line), _logger(logger) { std::stringstream ss; ss << "[" << _curr_time << "] " << "[" << Level2String(_level) << "] " << "[" << _pid << "] " << "[" << _filename << "] " << "[" << _line << "]" << " - "; _loginfo = ss.str(); } template<typename T> LogMessage& operator << (const T &info) { std::stringstream ss; ss << info; _loginfo += ss.str(); return *this; } ~LogMessage() { if(_logger._strategy) { _logger._strategy->SyncLog(_loginfo); } } private: std::string _curr_time; // 日志时间 LogLevel _level; // 日志等级 pid_t _pid; // 进程pid std::string _filename; int _line; std::string _loginfo; // 一条合并完成的,完整的日志信息 Logger &_logger; // 提供刷新策略的具体做法 }; LogMessage operator()(LogLevel level, std::string filename, int line) { return LogMessage(level, filename, line, *this); } ~Logger() { } private: std::unique_ptr<LogStrategy> _strategy; }; Logger logger; #define LOG(level) logger(level, __FILE__, __LINE__) #define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy() #define EnableFileLogStrategy() logger.EnableFileLogStrategy()

Read more

【C++深学日志】C++“类”的完全指南--从基础到实践(一)

【C++深学日志】C++“类”的完全指南--从基础到实践(一)

假想一下,你是一个顶级汽车设计师,你的任务不是亲自拧紧每一个螺丝,而是要设计出一幅“汽车蓝图”,你在图纸上设计了一辆汽车所需的一切:车轮、车灯、V8发动机、方向盘等,你手上这份设计好的蓝图就相当于我们今天要讲的C++中的“类”,它规定了汽车的属性(例如:离合器)和方法(功能:换挡),它本身并不是一辆真正的汽车,只是你的一份设计规划,后续你交付给工厂,工厂按照你的设计蓝图,生产出了一辆汽车,这就是实例化,后续工厂有根据你的蓝图设计了一条流水线,每一辆从流水线上生产下来的车辆,都是里这个蓝图(类)的一个对象,他们都有蓝图定义的属性和功能。在C++中类就充当着蓝图的作用,它定义了对象拥有哪些属性,那么就和我一起来揭开这份“蓝图”的面纱吧。 1.类 1.1.类的定义 类的基本思想是数据抽象和封装,数据抽象是一种依赖于接口和实现的分离式编程技术,类的接口包括用户所能执行的操作,类的实现则是包括类的数据成员、负责接口实现的函数以及定义类所需的各种私有函数。封装实现了类的接口和实现的分离,封装后的类隐藏了他的视线细节,也就是说,

By Ne0inhk
Qt步进电机上位机控制程序源代码:跨平台C/C++编写,支持多种端口类型与详细注释

Qt步进电机上位机控制程序源代码:跨平台C/C++编写,支持多种端口类型与详细注释

Qt步进电机上位机控制程序源代码Qt跨平台C/C++语言编写 支持串口Tcp网口Udp网络三种端口类型 提供,提供详细注释和人工讲解 1.功能介绍: 可控制步进电机的上位机程序源代码,基于Qt库,采用C/C++语言编写。 支持串口、Tcp网口、Udp网络三种端口类型,带有调试显示窗口,接收数据可实时显示。 带有配置自动保存功能,用户的配置数据会自动存储,带有超时提醒功能,如果不回复则弹框提示。 其中三个端口,采用了类的继承与派生方式编写,对外统一接口,实现多态功能,具备较强的移植性。 2.环境说明: 开发环境是Qt5.10.1,使用Qt自带的QSerialPort,使用网络的Socket编程。 源代码中包含详细注释,使用说明,设计文档等。 请将源码放到纯英文路径下再编译。 3.使用介绍: 可直接运行在可执行程序里的exe文件,操作并了解软件运行流程。 本代码产品特点: 1、尽量贴合实际应用,细节考虑周到。 2、注释完善,讲解详细,还有相关扩展知识点介绍。

By Ne0inhk
【C++】深入拆解二叉搜索树:从递归与非递归双视角,彻底掌握STL容器的基石

【C++】深入拆解二叉搜索树:从递归与非递归双视角,彻底掌握STL容器的基石

【C++】深入拆解二叉搜索树:从递归与非递归双视角,彻底掌握STL容器的基石 * 摘要 * 目录 * 一、概念 * 二、 性能分析 * 三、key结构非递归模拟实现 * 1. 二叉搜索树的插入 * 2. 二叉搜索树的查找 * 3. 二叉搜索树的删除 * 4. 二叉搜索树的中序遍历 * 四、key结构递归的模拟实现 * 1. 递归与非递归二叉搜索树核心操作的对比 * 2. 递归插入 * 3. 递归查找 * 4. 递归删除 * 总结 摘要 二叉搜索树(BST)是一种重要的数据结构,它通过"左子树所有节点值小于根节点,右子树所有节点值大于根节点"的特性实现高效的元素组织。本文详细解析了BST的核心概念、性能特点,并分别通过非递归和递归两种方式完整实现了插入、查找、删除等关键操作,深入探讨了指针引用在递归实现中的巧妙应用,以及两种实现方式在时间复杂度、空间复杂度和适用场景上的差异。 目录

By Ne0inhk
【C++】 —— 笔试刷题day_28

【C++】 —— 笔试刷题day_28

一、游游的重组偶数 题目解析 这道题,有q组数据,每一次输入一个正整数x,让我们将这个数进行重排,变成一个偶数,然后返回(如果x本身就是一个偶数那可以直接返回x); 如果不存在合法解,就是x通过重排后,无法变成一个偶数,就输出-1; 算法思路 这道题,总体来说还是比较简单的; 对于正整数x,我们可以把它当作一个字符串进行输入;(如果按照整数输入,我们还要将这个数x的每一位变换成对应数组) 我们知道,如果一个数是偶数,那最低位一定是一个偶数,这样我们只需判断字符串的最后一位即可知道这个数是否是偶数;如果这个数是偶数,那就直接输出即可;如果最后一位不是偶数,那就从第一位开始向后找,找到一位是偶数,然后把它交换到最后一位;然后输出即可;如果遍历完这个字符串,还没找到一位是偶数的,那就表示这个数x通过重拍无法变成偶数,输出-1即可。 题目解析 #include<iostream>usingnamespace std; string func(){ string str; cin >>

By Ne0inhk