【Linux】线程池(一)C++ 手写线程池:基于策略模式实现高性能日志模块

【Linux】线程池(一)C++ 手写线程池:基于策略模式实现高性能日志模块

文章目录


池化技术

池化技术可以减少很多的底层重复工作,例如创建进程、线程、申请内存空间时的系统调用和初始化工作,例如线程池,先预先创建好一些线程,当任务到来时直接将预先创建好的线程唤醒去处理任务,效率会远远高于任务到来时临时创建线程。例如内存池,但我们要用1mb空间时内存池会一次性申请20mb空间,效率会远远高于用多少空间申请多少空间(申请空间会调用系统调用)。
线程池是执行流级别的池化技术,STL中的空间配置器和内存池是内存块管理级别的池化技术。

线程池的日志模块

下⾯开始,我们结合我们之前所做的所有封装,进⾏⼀个线程池的设计。在写之前,我们要做如下准备。

  • 准备线程的封装
  • 准备锁和条件变量的封装
  • 引⼊日志,对线程进⾏封装

日志与策略模式

什么是设计模式
IT⾏业这么⽕, 涌⼊的⼈很多. 俗话说林⼦⼤了啥⻦都有. ⼤佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖⼤佬的后腿, 于是⼤佬们针对⼀些经典的常⻅的场景, 给定了⼀些对应的解决⽅案, 这个就是设计模式。

什么是日志
计算机中的日志是记录系统和软件运⾏中发⽣事件的⽂件,主要作⽤是监控运⾏状态、记录异常信息,帮助快速定位问题并⽀持程序员进⾏问题修复。它是系统维护、故障排查和安全管理的重要⼯具。
日志格式以下⼏个指标是必须得有的
时间戳
日志等级(严重程度)
日志内容
以下⼏个指标是可选的
⽂件名⾏号
进程,线程相关id信息等
⽇志有现成的解决⽅案,如:spdlog、glog、Boost.Log、Log4cxx等等,我们依旧采⽤⾃定义日志的⽅式。
这⾥我们采⽤设计模式中的策略模式来进⾏⽇志的设计,具体策略模式介绍,详情看下文。
我们想要的日志格式如下:

在这里插入图片描述

日志模块

两个核心问题

1、日志内容的刷新策略,刷新到显示器或文件或网络。
2、构建一条完整的日志。

设计文件等级

我们知道枚举类型中的枚举值(编译期常量)的底层存储是是整数,所以我们需要把它们转换成字符串输出。(补充:枚举值的类型就是枚举类本身)

//Logger.hpp// C++11支持的强枚举,访问成员需指定作用域,不易出现命名冲突enumclassLogLevel{ DEBUG, INFO,// 正常消息 WARING,// 出现错误,但不影响程序运行 ERROR,// 导致程序退出的错误 FATAL,// 重大错误}; std::string Level_to_string(LogLevel level){switch(level){case LogLevel::DEBUG:return"DEBUG";case LogLevel::INFO:return"INFO";case LogLevel::WARING:return"WARING";case LogLevel::ERROR:return"ERROR";case LogLevel::FATAL:return"FATAL";default:return"Unknown";}}

刷新策略

日志的刷新策略我们打算用策略模式来设计,策略模式其实就是利用C++的多态特性,先创建一个日志刷新基类,然后根据日志具体往哪刷新写具体的派生类函数,例如往显示器中写、往文件中写、往网络中写等等。

小编重点讲一下往文件中写,具体实现注意事项及步骤如下:
1、当我们要把日志内容刷新到文件中时需要在当前工作目录下新建一个文件夹,因为日志需要根据不同的日志等级把日志内容写到不同的文件里。
2、我们要新建文件夹首先可以用mkdir系统调用:

在这里插入图片描述


但是小编更推荐大家使用C++17提供的文件操作接口,需要包< filesystem >头文件,在派生类FileLogStrategy的构造函数里需要先将指定目录文件创建好,首先要判断在当前工作目录下指定目录文件存不存在,若存在直接返回,若存在则新建该指定目录文件,但是创建目录可能会遇到各种问题例如父目录不存在等等,所以我们创建目录时需要try-catch捕异常。
3、然后实现SyncLog往日志文件中刷新日志内容。首先我们要先拼接目标文件路径,然后使用C++风格的文件操作对指定文件写入日志内容。

下面是源码及测试代码:

//logger.hpp// 策略模式,策略接⼝classLogStrategy{public:virtual~LogStrategy()=default;// 纯虚函数,强制派生类重写该函数virtualvoidSyncLog(const std::string &logmessage)=0;};// 控制台日志策略,就是日志只向显示器打印,方便我们debugclassConsoleLogStrategy:publicLogStrategy{public:~ConsoleLogStrategy(){}voidSyncLog(const std::string &logmessage)override{//显示器是共享资源,需要加锁保护{ LockGuard lockguard(&_lock); std::cout << logmessage << std::endl;}}private: Mutex _lock;};// 日志向文件打印const std::string logdefaultpath ="log";conststatic std::string logdefaultfilename ="test.log";classFileLogStrategy:publicLogStrategy{public:FileLogStrategy(const std::string &dir = logdefaultpath,const std::string &filename = logdefaultfilename):_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()<<'\n';}}}voidSyncLog(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);//appendif(!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目录,再在log路径下形成hello.log文件,并把日志内容写到该文件中 Mutex _lock;};
//main.cc#include"Logger.hpp"intmain(){ std::string test ="hello logger";// //测试策略1,显示器写入// //智能指针,本质就是对 LogStrategy * 原生指针做了封装// std::unique_ptr<LogStrategy> logger_ptr = std::make_unique<ConsoleLogStrategy>();// logger_ptr->SyncLog(test);// logger_ptr->SyncLog(test);// logger_ptr->SyncLog(test);// logger_ptr->SyncLog(test);// logger_ptr->SyncLog(test);//测试策略2,文件写入//智能指针,本质就是对 LogStrategy * 原生指针做了封装 std::unique_ptr<LogStrategy> logger_ptr = std::make_unique<FileLogStrategy>(); logger_ptr->SyncLog(test); logger_ptr->SyncLog(test); logger_ptr->SyncLog(test); logger_ptr->SyncLog(test); logger_ptr->SyncLog(test);return0;}

获取日志时间

1、首先需要获取时间戳,利用time系统调用:

在这里插入图片描述


参数传递nullptr,返回值类型本质是对long int类型做的封装,返回值具体是1970-01-01午夜到现在的秒数。
2、然后根据时间戳,转化成可读性较强的时间信息,例如1970-01-01 00:00:00。实现该功能需要需要利用localtime函数,localtime函数有两种类型,后缀加_r和不加_r,加_r表示该函数可被重入,小编推荐使用加_r的。

在这里插入图片描述


它会将时间戳转化为struct tm结构体,第一个是输入型参数,传递时间戳,第一个是输出型参数,将结果通过指针带出,成功返回结构体指针,失败返回nullptr。
3、把年月日时分秒转化为字符串,我们用以前介绍过的snprintf:

在这里插入图片描述


但是一点需要注意,struct tm中的年剪掉了1900,月是0-11,需要我们手动把年加1900,把月加1。

// 根据时间戳,获取可读性较强的时间信息 std::string GetCurrentTime(){// 1、获取时间戳 time_t curtime =time(nullptr);// 2、把时间戳转化为年月日时分秒structtm currtm;localtime_r(&curtime,&currtm);// 3、把年月日时分秒转化为字符串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;}

logger类实现

现在所有零件都已加工完成,接下来需要把它们拼接成一个完整的日志类,供我们使用。

首先需要激活策略,要开启什么策略,就把策略对应的派生类对象创建出来,并把派生类成员指针变量指向该对象。

classLogger{public:Logger(){}//激活策略voidEnableConsoleLogStrategy(){ _strategy = std::make_unique<ConsoleLogStrategy>();}voidEnableFileLogStrategy(){ _strategy = std::make_unique<FileLogStrategy>();}~Logger(){}private: std::unique_ptr<LogStrategy> _strategy;};

现在我们还缺形成一条完整日志的方式,这里小编利用内部类来实现,在logger内部定义一个LogMessage的内部类,利用LogMessage将各种日志信息拼接成一个字符串,在LogMessage的析构函数中调用logger成员变量_strategy的刷新函数将字符串刷新出去,这也是RAII风格的设计思路,所以内部类还需要定义一个对外部类引用的成员变量。

形成一条完整日志后就需要将日志信息信息刷新出去,这里我们通过重载括号运算符实现,比定义一个具体writelog函数更优雅,具体分析见代码注释。

内部类LogMessage实现

LogMessage的构造函数:
首先将成员变量利用初始化列表初始化,然后用stringstream类对象ss将各种类型的成员变量格式化为字符串,接着调用对象ss的str接口,将其赋给_logiofo,构成日志信息的左半部分。
stringstream需要包<sstream>头文件,它是C++提供的格式化方案,前面我们用的snprintf是C语言提供的方案。
LogMessage的operator<<:
然后拼接_loginfo右半部分,首先我们先大致看一下未来的日志输出方式:

LOG(LogLevel::FATAL)<<"hello world"<<1234<<", 3.14"<<'c';

我们可以看到右半部分是参数是可变的,并且参数类型也不确定,所以处理思路是在内部类中对输出运算符做重载,并且参数类型设置成模板,这样任意类型参数都可以输出。
LogMessage的析构函数:
当LogMessage调用operator<<完毕后,_loginfo也就拼接完毕了,此时LogMessage要析构了,我们就可以在LogMessage的析构函数中将_loginfo的日志信息刷新出去。

日志刷新流程图及源码

在这里插入图片描述
//logger.hpp#pragmaonce#include<iostream>#include<filesystem>//C++17#include<fstream>#include<string>#include<sstream>#include<ctime>#include<memory>#include<unistd.h>#include"Mutex.hpp"// C++11支持的强枚举,访问成员需指定作用域,不易出现命名冲突enumclassLogLevel{ DEBUG, INFO,// 正常消息 WARING,// 出现错误,但不影响程序运行 ERROR,// 导致程序退出的错误 FATAL,// 重大错误}; std::string Level_to_string(LogLevel level){switch(level){case LogLevel::DEBUG:return"DEBUG";case LogLevel::INFO:return"INFO";case LogLevel::WARING:return"WARNING";case LogLevel::ERROR:return"ERROR";case LogLevel::FATAL:return"FATAL";default:return"Unknown";}}// 根据时间戳,获取可读性较强的时间信息 std::string GetCurrentTime(){// 1、获取时间戳 time_t curtime =time(nullptr);// 2、把时间戳转化为年月日时分秒structtm currtm;localtime_r(&curtime,&currtm);// 3、把年月日时分秒转化为字符串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;}/////////////////////////////////////////////////////////////// 策略模式,策略接⼝classLogStrategy{public:virtual~LogStrategy()=default;// 纯虚函数,强制派生类重写该函数virtualvoidSyncLog(const std::string &logmessage)=0;};// 控制台日志策略,就是日志只向显示器打印,方便我们debugclassConsoleLogStrategy:publicLogStrategy{public:~ConsoleLogStrategy(){}voidSyncLog(const std::string &logmessage)override{// 显示器是共享资源,需要加锁保护{ LockGuard lockguard(&_lock); std::cout << logmessage << std::endl;}}private: Mutex _lock;};// 日志向文件打印const std::string logdefaultpath ="log";conststatic std::string logdefaultfilename ="test.log";classFileLogStrategy:publicLogStrategy{public:FileLogStrategy(const std::string &dir = logdefaultpath,const std::string &filename = logdefaultfilename):_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()<<'\n';}}}voidSyncLog(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);// appendif(!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目录,再在log路径下形成hello.log文件,并把日志内容写到该文件中 Mutex _lock;};////////////////////////////////////////////////////////////classLogger{public:Logger(){}// 激活策略voidEnableConsoleLogStrategy(){ _strategy = std::make_unique<ConsoleLogStrategy>();}voidEnableFileLogStrategy(){ _strategy = std::make_unique<FileLogStrategy>();}//形成一条完整的日志信息,利用内部类LogMessageclassLogMessage{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 <<"] "<<"["<<Level_to_string(_level)<<"] "<<"["<< _pid <<"] "<<"["<< _filename <<"] "<<"["<< _line <<"] "; _loginfo = ss.str();}template<typenameT> LogMessage &operator<<(const T &info){ std::stringstream ss; ss << info; _loginfo += ss.str();return*this;}~LogMessage(){//如果外部类的_strategy的成员变量不为空,//就可以将_loginfo的内容通过_strategy刷新出去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;//对外部类引用,利用Logger为LogMessage提供刷新策略};//Logger提供的写日志方法 LogMessage operator()(LogLevel level, std::string filename,int line){//返回LogMessage的匿名对象returnLogMessage(level, filename, line,*this);}//上面LogMessage operator()实现引出的一些知识点://1、直接返回LogMessage临时对象,和先显式创建对象再返回的写法//在绝大多数场景下效果完全一致,返回临时对象的写法//编译器会直接在 “接收返回值的位置” 构造 LogMessage 对象,无任何拷贝/移动//先显式创建对象再返回的写法编译器会触发 RVO 优化,同样省略 log_msg 的拷贝 / 移动,//最终效果和写法 1 完全一致;即使关闭优化,C++11 也会通过移动构造函数//(LogMessage 无自定义移动构造时,编译器自动生成)完成返回,性能损耗可忽略。//2、的生命周期并非简单的 “只有一行”,它的存活时间会根据上下文被编译器延长,//尤其是在函数返回的场景下,例如上面代码的LogMessage匿名对象//3、这里写日志方法完全可以不重载括号运算符,而是写成一个具体函数如WriteLog,//这样就只在:#define LOG(level) logger.WriteLog(level, __FILE__, __LINE__)//需要修改一下调用方法,而重载括号运算符的会更优雅,小编更推荐~Logger(){}private: std::unique_ptr<LogStrategy> _strategy;};//先定义一个Logger对象,后面定义宏会用到 Logger logger;//定义调用日志的宏#defineLOG(level)logger(level,__FILE__,__LINE__)//定义选择日志模式的宏(也就是选择日志刷新策略)#defineEnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()#defineEnableFileLogStrategy() logger.EnableFileLogStrategy()
//main.cc#include"Logger.hpp"#include<unistd.h>intmain(){//预处理阶段时EnableConsoleLogStrategy()会被宏替换为logger.EnableConsoleLogStrategy()EnableConsoleLogStrategy();// EnableFileLogStrategy();LOG(LogLevel::ERROR)<<"hello world"<<1234<<", 3.14 "<<'c';LOG(LogLevel::WARING)<<"hello world"<<1234<<", 3.14 "<<'c';LOG(LogLevel::ERROR)<<"hello world"<<1234<<", 3.14 "<<'c';LOG(LogLevel::ERROR)<<"hello world"<<1234<<", 3.14 "<<'c';// std::string test = "hello logger";// //测试策略1,显示器写入// //智能指针,本质就是对 LogStrategy * 原生指针做了封装// std::unique_ptr<LogStrategy> logger_ptr = std::make_unique<ConsoleLogStrategy>();// logger_ptr->SyncLog(GetCurrentTime());// sleep(1);// logger_ptr->SyncLog(GetCurrentTime());// sleep(1);// logger_ptr->SyncLog(GetCurrentTime());// sleep(1);// logger_ptr->SyncLog(GetCurrentTime());// sleep(1);// logger_ptr->SyncLog(GetCurrentTime());// sleep(1);// //测试策略2,文件写入// //智能指针,本质就是对 LogStrategy * 原生指针做了封装// std::unique_ptr<LogStrategy> logger_ptr = std::make_unique<FileLogStrategy>();// logger_ptr->SyncLog(test);// logger_ptr->SyncLog(test);// logger_ptr->SyncLog(test);// logger_ptr->SyncLog(test);// logger_ptr->SyncLog(test);return0;}

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的关注和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

在这里插入图片描述

Read more

Flutter 三方库 changelog_cli 的鸿蒙化适配指南 - 自动化生成 CHANGELOG、标准化版本管理与工程化协作利器

Flutter 三方库 changelog_cli 的鸿蒙化适配指南 - 自动化生成 CHANGELOG、标准化版本管理与工程化协作利器

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 changelog_cli 的鸿蒙化适配指南 - 自动化生成 CHANGELOG、标准化版本管理与工程化协作利器 前言 在 Flutter for OpenHarmony 的企业级开发流程中,维护一份详实、规范的更新日志(CHANGELOG)是版本控制的核心环节。changelog_cli 是一个专为 Flutter 开发者设计的命令行工具,它能够基于特定的规范自动生成或更新日志。本文将探讨如何将该工具集成到鸿蒙项目的开发流水线中,大幅提升工程化协作效率。 一、原理解析 / 概念介绍 1.1 基础原理 changelog_cli 通过读取项目的 pubspec.yaml 版本信息和特定的配置文件,配合开发者在命令行输入的更新内容,自动拼装成符合 Keep a Changelog 规范的

By Ne0inhk
中小团队如何低成本搭建项目管理系统?基于 Ubuntu 的 Dootask 私有化部署实战

中小团队如何低成本搭建项目管理系统?基于 Ubuntu 的 Dootask 私有化部署实战

作为技术负责人或者创业团队的 Team Leader,你是否也经历过这样的“项目管理噩梦”? 团队规模刚过 10 人,管理瞬间失控。需求变了没记录,Bug 修复进度全靠吼,代码上线版本混乱。老板让你上一套项目管理系统,你调研了一圈发现:Jira 太贵且对非技术人员极不友好;禅道功能强大但界面由于年代久远,操作逻辑繁琐,推行下去阻力巨大,运营和设计同事天天抱怨学不会;市面上的 SaaS 工具(如 Teambition)虽然好用,但核心数据存在别人云端,想要二次开发或私有化部署,授权费又是一笔不小的开支。 这其实是很多中小团队的共性痛点:需要一个好用的开源项目管理工具,既要免费开源、数据私有化,又要界面现代、部署简单。 为了帮大家理清思路,我画了一张当前团队协作常见困境的思维导图,看看你是否中招了: 最近在为团队寻找替代方案时,我在 GitHub 上发现了一个宝藏项目——DooTask。目前它在 GitHub 上已经获得了 4k+ Star,这不仅代表了社区认可度,

By Ne0inhk

Flutter 组件 rexios_lints 适配鸿蒙 HarmonyOS 实战:代码工艺化治理,构建编译期的架构合规防线

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 rexios_lints 适配鸿蒙 HarmonyOS 实战:代码工艺化治理,构建编译期的架构合规防线 前言 在鸿蒙(OpenHarmony)生态迈向大规模团队协同、涉及分布式跨端开发与高频业务迭代的背景下,如何确保代码质量的底线、统一多人的编程风格并拦截潜在的运行时陷阱,已成为决定项目长效生命力的“基础设施”。在鸿蒙设备这类对应用稳定性与资源占用有严苛要求的环境下,如果缺乏强力的静态代码分析(Lints)约束,由于由于开发者习惯差异导致的异步坑洞、内存泄漏或命名碎片化,将直接侵蚀鸿蒙系统的运行流畅度。 我们需要一种能够超越官方默认规则、具备“架构审判”级别严密度且可高度定制的静态分析套件。 rexios_lints 为 Flutter 开发者提供了一套极其严苛且符合现代工程实践的 Lint 规则集。它不仅涵盖了基础的代码格式校验,更深入到异步编程(Future/Stream)安全、强类型检查等核心架构领域。在适配到鸿蒙 Harmon

By Ne0inhk
Flutter 三方库 bybit 的鸿蒙化适配指南 - 实现高性能交易数据获取、支持 WebSockets 实时订单簿与加密货币交易接口集成

Flutter 三方库 bybit 的鸿蒙化适配指南 - 实现高性能交易数据获取、支持 WebSockets 实时订单簿与加密货币交易接口集成

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 bybit 的鸿蒙化适配指南 - 实现高性能交易数据获取、支持 WebSockets 实时订单簿与加密货币交易接口集成 前言 在进行 Flutter for OpenHarmony 的金融科技(FinTech)应用开发时,对接主流交易所的实时数据和交易功能是核心需求。bybit 是一个专为 Bybit 交易所设计的异步 Dart SDK。它封装了 REST API 调用和复杂的 WebSockets 订阅逻辑。本文将探讨如何在鸿蒙系统下构建低延迟、高可靠的加密资产交易终端。 一、原原理分析 / 概念介绍 1.1 基础原理 bybit 库基于 http 处理基础请求,并利用 web_socket_

By Ne0inhk