当前位置: 首页 > news >正文

【Linux庖丁解牛】— 日志进程池 !

1. 日志与策略模式

1.1 认识日志

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

日志格式以下几个指标是必须得有的 • 时间戳• 日志等级 • 日志内容

以下几个指标是可选的 • 文件名行号 • 进程,线程相关id信息等

日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx等等,我们依旧采用自定义日志的方式。 这里我们采用设计模式-策略模式来进行日志的设计。

以下就是我们最终设计的日志格式:

1.2 什么是策略模式

官方说法:策略模式(Strategy Pattern)是一种行为型设计模式,其核心思想是将一系列算法封装成独立的类,并通过一个共同的接口使它们可以相互替换,从而实现算法的动态选择和解耦。

在我们的日志实现中,我们不仅想要将我们的日志信息打印到显示器中,还想要将我们的日志信息写入到指定文件中,我们希望可以根据自己的需求切换日志的写入方式。那么,我们可以采用策略模式来设计我们的日志具体做法就是实现一个基类,实现两个子类继承基类重写写入方法。然后,在我们的日志类当中为外部提供选择刷新方案的接口【显示器或指定文件】。

2. 基于策略模式的自定义日志 

2.1 策略实现

上面已经具体说了实现思路,这里直接上代码:

2.2 日志类logger实现

> 策略选择接口

在日志类内部我们需要基类指针,那我们这里选择用智能指针。

对外提供策略选择接口: 

在构造日志类的时候,我们默认使用显示器策略:

> 内部类log_message实现

这里为什么要使用内部类呢??

主要是为了方便构建一条日志信息,我们在logger类中重载()方法,用户在外部传递需要的信息,比如:日志等级,行号,文件名。而在()方法中,我们构建一个临时log_message对象,我们将用户信息传给这个临时对象。而这个临时对象内部重载了<<方法,该方法负责获取用户的打印信息,最终这个临时对象会处理这些信息拼接起来形成一条日志。在这个临时对象析构的时候,我们在析构函数中调用刷新方法。至此,一条日志信息就可以按照我们的需求刷新了!

这样设计主要是方便上层的使用,应为我们还可以在类内部定义宏用__FILE__和__LINE__来获取行号和当前文件名。而上层只需要传自己想要的日志等级和debug信息即可。

如果我们直接在logger内部重载<<,那用户使用起来就太不方便了:每次都要传参构造对象,然后使用对象写入信息。虽然也可以使用匿名对象直接<<,但还是要传那么多参数,太龊了。也没有办法定义像上面的宏,因为这是全局的变量。每条日志都要使用的话,里面的日志信息每次使用都要清空【每次<<调用都刷新到文件中】,但这样打印到屏幕的信息就不是我们习惯的行刷新了……额,这里就不做考虑了。

2.3 日志log.hpp源码

主要的实现思路有了,下面就是实现的一些具体代码了,这考验的就是我们的编码能力了~~

#pragma once#include <iostream>
#include <string>
#include <fstream>
#include <memory>
#include <ctime>
#include <sstream>
#include <filesystem>
#include <unistd.h>
#include "mutex.hpp"namespace log_module
{using namespace mutex_module;const std::string default_path = "./log/";const std::string default_name = "my.log";// 基类 刷新策略class log_strategy{public:virtual ~log_strategy() = default;virtual void sync_log(const std::string &message) = 0;};// 子类 向显示器刷新class screen_strategy : public log_strategy{public:void sync_log(const std::string &message) override{// 显示器是共享资源,加锁维护lock_guard lg(_mutex);std::cout << message << std::endl;}~screen_strategy(){}private:mutex _mutex;};// 子类 向指定文件刷新class file_strategy : public log_strategy{public:file_strategy(const std::string &path = default_path, const std::string &name = default_name): _path(path),_name(name){// 如果路径不存在则需要新建路径lock_guard lockguard(_mutex);if (filesystem::exists(_path))return;try{std::filesystem::create_directories(_path);}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << '\n';}}// 将⼀条日志信息写入到文件中void sync_log(const std::string &message) override{lock_guard lockguard(_mutex);std::string log = _path + _name;std::ofstream out(log.c_str(), std::ios::app); // 追加式if (!out.is_open())return;out << message << "\n";out.close();}~file_strategy(){}private:mutex _mutex;std::string _path;std::string _name;};// 日志等级enum class log_level{Debug,   // 测试Info,    // 普通信息Warning, // 告警Error,   // 错误Fatal    // 致命};std::string level_to_string(log_level level){switch (level){case log_level::Debug:return "Debug";case log_level::Info:return "Info";case log_level::Warning:return "Warning";case log_level::Error:return "Error";case log_level::Fatal:return "Fatal";default:return "UnKnown";}}std::string get_cur_time(){time_t cur_time = time(nullptr);struct tm ret;localtime_r(&cur_time, &ret);char buffer[128];snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d:%02d:%02d",ret.tm_year + 1900,ret.tm_mon + 1,ret.tm_mday,ret.tm_hour,ret.tm_min,ret.tm_sec);return buffer;}// 日志类class logger{public:logger(){// 默认刷新策略using_screen_strategy();}~logger(){}// 选择刷新策略接口void using_screen_strategy(){_strategy = std::make_unique<screen_strategy>();}void using_file_strategy(){_strategy = std::make_unique<file_strategy>();}// 内部日志类log_message->方便logger类创建一个临时log_message对象// ->临时对象销毁时相对应文件中刷新内容class log_message{private:std::string _cur_time;  // 当前时间log_level _level;       // 日志等级pid_t _pid;             // 进程idstd::string _file_name; // 对应的文件名int _line_num;          // 对应文件行号std::string _log_info;  // 日志信息logger &_logger;        // 拿到外部类方便采用策略刷新// 这里加引用才行,不加引用的话,本质是要去构造一个logger对象,但是你的log_message在内部,并不能看到完整的logger声明,所有无法构造出完整的logger对象public:// 构造函数log_message(log_level level, std::string &file_name, int line_num, logger &log): _cur_time(get_cur_time()),_level(level),_pid(getpid()),_file_name(file_name),_line_num(line_num),_logger(log){// 构造信息左半部分 [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] -std::stringstream ss;ss << "[" << _cur_time << "] "<< "[" << level_to_string(_level) << "] "<< "[" << _pid << "] "<< "[" << _file_name << "] "<< "[" << _line_num << "] - ";_log_info = ss.str();}// 重载 <<,返回值必须为引用template <typename T>log_message &operator<<(const T &info){std::stringstream ss;ss << info;_log_info += ss.str();return *this;}// 析构-> 采用指定策略刷新~log_message(){if (_logger._strategy){_logger._strategy->sync_log(_log_info);}}};// 重载logger(),返回值故意写成拷贝构建临时对象log_message operator()(log_level level, std::string file_name, int line_num){return log_message(level, file_name, line_num, *this); // 传*this,就是传logger对象}private:std::unique_ptr<log_strategy> _strategy;};// 定义全局的logger对象logger log;#define LOG(level) log(level, __FILE__, __LINE__)#define using_screen_strategy() log.using_screen_strategy();
#define using_file_strategy() log.using_file_strategy();};

3. 线程池

线程池:

⼀种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

线程池的应用场景:

• 需要大量的线程来完成任务,且完成任务的时间比较短。比如WEB服务器完成网页请求这样的任 务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象⼀个热门网站 的点击次数。但对于长时间的任务,比如⼀个Telnet连接请求,线程池的优点就不明显了。因为 Telnet会话时间比线程的创建时间大多了。

• 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。

• 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.

线程池的种类

a. 创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中 的任务接口

b. 浮动线程池,其他同上此处,我们选择固定线程个数的线程池。

此处,我们选择设计固定线程个数的线程池。

4. 单例模式

某些类,只应该具有⼀个对象(实例),就称之为单例.

在很多服务器开发场景中,经常需要让服务器加载很多的数据(上百G)到内存中.此时往往要用⼀个单例的类来管理这些数据.

> 饿汉实现方式和懒汉实现方式

吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下⼀顿吃的时候可以立刻拿着碗就能吃饭. 吃完饭, 先把碗放下, 然后下⼀顿饭用到这个碗了再洗碗, 就是懒汉方式。

懒汉方式最核心的思想就是“延时加载”这可以优化服务器的启动速度。我们其实早就见过“延时加载”机制了。Linux操作系统就是采用这种机制分配内存的,当我们使用malloc或new的时候,操作系统并没有立即为我们真正的分配内存空间【但是有虚拟地址】,而是在我们实际使用的时候才分配内存空间的。系统这样做可以让物理内存实实际际的被使用,而不会造成站着茅坑不拉屎的现象!

但是具体到代码中,懒汉实现方式如何做到呢??

我们需要使用到静态成员和静态方法。静态成员和静态方法有一个特点:在程序加载到内存时,系统就已经为其分配好内存空间了【即静态变量已经完成定义和初始化】,即使我们没有定义初始化这个包含静态成员的对象

所以,我们可以在类内定义一个指针,该指针的类型就是自己。我们在类外将该指针初始化为nullptr,然后,我们还要在类内提供public获取单例的方法,该方法也是静态的。方法内部会判断单例指针是否为空,为空则new出单例并返回该指针,不为空则说明该单例已近被创建直接返回指外部也可以获得单例指针。

template <typename T>
class Singleton
{static T *inst;public:static T *GetInstance(){if (inst == NULL){inst = new T();}return inst;}
};

还有,就是为了确保该类型只有一个对象, 类中的构造和拷贝构造以及赋值重载都必须是私有的!!

 5. 基于单例模式自定义线程池

线程池本身并不难设计,无非就是维护固定数量的线程,在访问临界资源时进行加锁,使用条件变量维护多线程的同步。下面就直接给代码了:

> pthread_pool.hpp

#pragma once#include "log.hpp"
#include "mutex.hpp"
#include "cond.hpp"
#include "pthread.hpp"
#include <vector>
#include <queue>using namespace log_module;
using namespace mutex_module;
using namespace cond_module;
using namespace pthread_module;static const int gnum = 5;namespace pthread_pool_module
{template <typename T>class pthread_pool{private:void wake_up_all_pthread(){lock_guard lg(_mutex);if (_sleep_num <= 0)return;// 唤醒所有线程_cond.broadcast();LOG(log_level::Info) << "唤醒所有休眠的线程";}void wake_up_one(){if (_sleep_num == 0)return;_cond.signal();}// 线程池启动也私有,一旦单例创建成功就启动线程池void start(){if (_isrunning == true)return;_isrunning = true;for (auto &e : _pthreads){e.start();LOG(log_level::Info) << e.name() << "线程创建成功";}}// 构造私有pthread_pool(int num = gnum): _num(num),_isrunning(false),_sleep_num(0){for (int i = 0; i < num; i++){_pthreads.emplace_back([this](){handler_task();});}}// 拷贝构造和赋值重载私有【保证线程池只有一个对象】pthread_pool(const pthread_pool<T> &) = delete;pthread_pool<T> &operator=(const pthread_pool<T> &) = delete;public:// 静态获取单例static pthread_pool<T> *get_instance(){// 单例已经生成就没有必要竞争锁了if (_pt == nullptr){LOG(log_level::Info) << "尝试获取单例";// 单例首次生成可能被多线程并发执行,加锁保护lock_guard lg(_lock);if (_pt == nullptr){_pt = new pthread_pool<T>();LOG(log_level::Info) << "首次获取单例成功";_pt->start();}}return _pt;}void join(){for (auto &e : _pthreads){e.join();LOG(log_level::Info) << e.name() << "线程join成功";}}void stop(){if (_isrunning == false)return;_isrunning = false;wake_up_all_pthread();}void handler_task(){char name[128];pthread_getname_np(pthread_self(), name, sizeof(name));while (true){T t;{lock_guard lg(_mutex);// 线程休眠条件:1.任务队列为空 2.线程池运行while (_taskq.empty() && _isrunning == true){_sleep_num++;_cond.wait(_mutex);_sleep_num--;}// 线程池退出条件 1.任务队列为空 2.线程池退出if (_isrunning == false && _taskq.empty()){LOG(log_level::Info) << name << "退出了," << "线程池退出了";break;}t = _taskq.front();_taskq.pop();}t(); // 处理任务不需要加锁}}// 入任务bool equeue(const T &t){if (!_isrunning)return false;lock_guard lg(_mutex);_taskq.push(t);if (_sleep_num == num)wake_up_one();return true;}~pthread_pool() {}private:std::vector<pthread> _pthreads; // 维护多线程int _num;                       // 线程个数std::queue<T> _taskq;           // 任务队列mutex _mutex;                   // 互斥锁cond _cond;                     // 条件变量bool _isrunning;                // 线程池运行状态int _sleep_num;                 // 休眠线程个数static pthread_pool<T> *_pt; // 静态生成类内指针static mutex _lock;          // 静态生成锁};// 初始化类内静态成员template <typename T>pthread_pool<T> *pthread_pool<T>::_pt = nullptr;template <typename T>mutex pthread_pool<T>::_lock;
}

> main.cc

#include "log.hpp"
#include "pthread_pool.hpp"
#include "task.hpp"using namespace log_module;
using namespace pthread_pool_module;// using task_t = function<void *(void *)>;int main()
{LOG(log_level::Debug) << "for test";// 获取单例pthread_pool<task_t> *p = pthread_pool<task_t>::get_instance();pthread_pool<task_t> *p2 = pthread_pool<task_t>::get_instance();// printf("%p\n",p);// printf("%p\n",p2);int cnt = 5;while (cnt--){p->equeue(download);}p2->stop();p2->join();return 0;
}
http://www.dtcms.com/a/299947.html

相关文章:

  • 大模型系列——Dify:知识库与外部知识库
  • SSH连接失败排查与解决教程: Connection refused
  • PromQL完全指南:掌握Prometheus核心查询语言
  • Ubuntu 22.04 配置 Zsh + Oh My Zsh + Powerlevel10k
  • 二十八、【Linux系统域名解析】DNS安装、子域授权、缓存DNS、分离解析、多域名解析
  • C++___快速入门(上)
  • 人工智能之数学基础:概率论之韦恩图的应用
  • WebAPIs里的filter
  • Android 编码规范全指南
  • 驱动-设备树-基本语法
  • Python爬虫实战:诗词名句网《三国演义》全集
  • 服务器:数字世界的隐形引擎
  • 《基于雅可比矢量近似的EIT触觉传感灵敏度非均匀校正》论文解读
  • ESP32实战:5分钟实现PC远程控制LED灯
  • C++类和对象(三)
  • IC测试之pogo pin学习与总结-20250726
  • 进制定义与转换详解
  • 1.Java发展简史与设计哲学
  • 最优估计准则与方法(5)加权最小二乘估计(WLS)_学习笔记
  • 360° 外壁镜头:小物体环外侧检测的创新突破
  • Python day25
  • MySQL中的 redolog
  • 连锁店铺巡查二维码的应用
  • 单片机CPU内部的定时器——滴答定时器
  • 智慧水库边缘计算技术路线与框架设计
  • 21-ospf多区域
  • Python编程:初入Python魔法世界
  • Java 面向对象之方法与方法重载:从基础到实践
  • Go 多模块仓库标签管理教程
  • 详解Aerospike数据库在Linux系统上的安装流程