【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

【Linux】线程同步

【Linux】线程同步

📝前言: 上篇文章我们讲解了【Linux】线程互斥,这篇文章我们来讲讲Linux——线程同步 🎬个人简介:努力学习ing 📋个人专栏:Linux 🎀ZEEKLOG主页 愚润求学 🌄其他专栏:C++学习笔记,C语言入门基础,python入门基础,C++刷题专栏 目录 * 一,同步定义 * 二,条件变量 * 1. 基本介绍 * 2. 接口 * 2.1 初始化和销毁 * 初始化 * 销毁 * 2.2 等待和通知 * 等待 * 通知(唤醒) * 三,使用经典规范 * 1. 等待代码 * 2. 唤醒代码 * 四,生产者消费者模型 * 1. 基本介绍 * 2.

By Ne0inhk
08-OpenClaw自动化与定时任务

08-OpenClaw自动化与定时任务

OpenClaw 自动化与定时任务 免费专栏全套教程:OpenClaw从入门到精通 OpenClaw 提供了一套完整的自动化系统,包括 Heartbeat 心跳机制、Cron 定时任务、Hooks 事件钩子和 Webhook 外部触发。本章将详细介绍这些机制的概念、配置和实战应用。 目录 1. 自动化工作流概念 2. Heartbeat 心跳机制 3. Cron 定时任务配置 4. Hooks 事件钩子 5. Webhook 外部触发 6. 实战案例 7. 故障排查 1. 自动化工作流概念 1.1 核心组件 OpenClaw 的自动化系统由四个核心组件构成: 组件用途触发方式适用场景Heartbeat周期性检查自动定时批量检查、上下文感知监控Cron精确定时任务时间驱动固定时间执行、独立任务Hooks事件驱动响应事件触发命令响应、生命周期管理Webhook外部系统集成HTTP 请求第三方系统对接、推送接收 1.

By Ne0inhk
Flutter 三方库 sort_json 的鸿蒙化适配指南 - 实现 JSON 键值的自动化递归排序、支持规范化输出与项目配置文件清理

Flutter 三方库 sort_json 的鸿蒙化适配指南 - 实现 JSON 键值的自动化递归排序、支持规范化输出与项目配置文件清理

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 sort_json 的鸿蒙化适配指南 - 实现 JSON 键值的自动化递归排序、支持规范化输出与项目配置文件清理 前言 在进行 Flutter for OpenHarmony 的工程化开发时,保持项目配置文件(如 package.json、.json5 或各种国际化语言文件)的条理性是至关重要的。特别是在多人协作或版本控制(Git)中,无序的 JSON 键值会导致严重的冲突。sort_json 是一个专注于将 JSON 字符串或文件重新排版并按字母顺序排序的库。本文将探讨如何利用该工具优化鸿蒙项目的配置管理。 一、原理解析 / 概念介绍 1.1 基础原理 sort_json 通过将输入的 JSON

By Ne0inhk
分享个人制作的Openclaw 2026.3.7 Docker离线部署方案

分享个人制作的Openclaw 2026.3.7 Docker离线部署方案

分享个人制作的Openclaw 2026.3.7 Docker离线部署方案 文档编辑时间:2026-3-8 1、下载镜像 个人分享的镜像,保证无毒无木马,基于node:22-bookworm镜像制作。 网盘地址: https://pan.baidu.com/s/1RqyskudGPxCPdpxvCQ7mzQ?pwd=c1us 提取码: c1us 2、导入镜像 docker load --input openclaw-2026.3.7.images 3、修改配置 在linux服务器/home/openclaw/docker/default/目录下面创建一个.openclaw文件夹,在里面创建openclaw.json文件,当然这个目录你可以自己指定,内容如下: {"meta":{"

By Ne0inhk