【Linux学习笔记】日志器与线程池设计
【Linux学习笔记】日志器与线程池设计

🔥个人主页:大白的编程日记
🔥专栏:Linux学习笔记

文章目录
- 【Linux学习笔记】日志器与线程池设计
- 前言
- 3-1日志与策略模式
- 3-1-1 什么是设计模式
- 3-1-2 日志认识
- 3-1-3 Log.hpp
- 3-1-4 策略模式
- 3-2 线程池设计
- 3-2-1 线程池:
- 3-2-2 线程池的应用场景:
- 3-2-3 线程池的种类
- 3-2-4 ThreadPool.hpp
- 后言
前言
哈喽,各位小伙伴大家好!上期我们讲了阻塞队列和环形队列 今天我们讲的是日志器与线程池设计。话不多说,我们进入正题!向大厂冲锋!
- 线程池
下面开始,我们结合我们之前所做的所有封装,进行一个线程池的设计。在写之前,我们要做如下准备
- 准备线程的封装
- 准备锁和条件变量的封装
- 引入日志,对线程进行封装
3-1日志与策略模式
3-1-1 什么是设计模式
IT行业这么火,涌入的人很多.俗话说林子大了啥鸟都有.大佬和菜鸡们两极分化的越来越严重.为了让菜鸡们不太拖大佬的后腿,于是大佬们针对一些经典的常见的场景,给定了一些对应的解决方案,这个就是设计模式
3-1-2 日志认识
计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。
日志格式以下几个指标是必须得有的
时间戳
日志等级日志内容
以下几个指标是可选的
文件名行号进程,线程相关id信息等
日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx等等,我们依旧采用自定义日志的方式。
这里我们采用设计模式-策略模式来进行日志的设计,具体策略模式介绍,详情看代码和课程。
我们想要的日志格式如下:
1 [可读性很好的时间] [日志等级] [进程pid] [打印对应日志的文件名][行号] - 消息内容,支持可变参数
2 [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
3 [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [17] - hello world
4 [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world
5 [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world
6 [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [21] - hello world
7 [2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world
3-1-3 Log.hpp
因为我们的日志可以想显示器打印也可以向日志打印
所以这里我们采用策略模式 定义一个基类和一个刷新函数虚函数
由日志类和显示器类重写虚函数
#ifndef __LOG_HPP__
#define __LOG_HPP__#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>
#include "Mutex.hpp"
using namespace std;namespace LogModule
{using namespace MutexModule; // 使用互斥锁模块const std::string gsep = "\r\n"; // 全局分隔符// 策略模式,C++多态特性// 2. 刷新策略 a: 显示器打印 b:向指定的文件写入// 刷新策略基类class LogStrategy{public:virtual void SyncLog(const std::string &message) = 0; // 纯虚函数,定义日志刷新接口};// 显示器刷新日志类 重写虚函数class ConsoleLogstrategy : public LogStrategy{public:virtual void SyncLog(const std::string &message) // 重写日志刷新函数{LockGuard guard(_mutex); // 使用互斥锁保护cout << message << endl; // 在控制台打印日志}private:Mutex _mutex; // 互斥锁,防止多线程冲突};// 1. 形成日志等级enum class LogLevel // 日志等级枚举{DEBUG,INFO,WARNING,ERROR,FATAL};// 把日志登记转为字符串string Level2str(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";}}// 把当前时间戳转为固定格式的字符串std::string GetTimeStamp() // 获取当前时间戳{time_t curr = time(nullptr); // 获取当前时间struct tm curr_tm;localtime_r(&curr, &curr_tm); // 转换为本地时间char timebuff[128];snprintf(timebuff, 128, "%4d-%02d-%02d-%02d-%02d-%02d", // 格式化时间curr_tm.tm_year + 1900,curr_tm.tm_mon + 1,curr_tm.tm_mday,curr_tm.tm_hour,curr_tm.tm_min,curr_tm.tm_sec);return timebuff;}// 文件打印日志的策略 : 子类const std::string defaultpath = "./log"; // 默认日志文件夹路径const std::string defaultfile = "my.log"; // 默认日志文件名class FileLogStrategy : public LogStrategy // 文件日志策略类{public:FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile) // 构造函数: _path(path), _file(file){if (std::filesystem::exists(_path)) // 检查路径是否存在{return;}try{std::filesystem::create_directories(_path); // 创建日志文件夹}catch (const std::filesystem::filesystem_error &e) // 捕获异常{cout << e.what() << endl;}}// 文件刷新函数 打开文件流输入virtual void SyncLog(const std::string &message) // 重写日志刷新函数{LockGuard guard(_mutex); // 使用互斥锁保护string filename = _path + (_path.back() == '/' ? "" : "/") + _file; // 构造文件路径ofstream of(filename, ios::app); // 打开文件流if (!of.is_open()) // 检查文件是否打开成功{return;}of << message << endl; // 写入日志of.close(); // 关闭文件流}private:string _path; // 文件目录string _file; // 文件路径Mutex _mutex; // 互斥锁,防止多线程冲突};// 日志类class Logger{public:Logger() // 构造函数{EnableConsoleLogStrategy(); // 默认启用控制台日志策略}void EnableFileLogStrategy() // 启用文件日志策略{_fflush_strategy = make_unique<FileLogStrategy>(); // 创建文件日志策略对象}void EnableConsoleLogStrategy() // 启用控制台日志策略{_fflush_strategy = make_unique<ConsoleLogstrategy>(); // 创建控制台日志策略对象}// 模版日志信息内部类 通过内部类拿到日志类class LogMessage // 日志消息类{public:LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger) // 构造函数: _level(level),_line_number(line_number),_src_name(src_name),_logger(logger),_pid(getpid()), // 获取当前进程ID_curr_time(GetTimeStamp()) // 获取当前时间戳{stringstream ss;ss << "[" << _curr_time << "] " // 构造日志头<< "[" << Level2str(_level) << "] "<< "[" << _pid << "] "<< "[" << _src_name << "] "<< "[" << _line_number << "] "<< "- ";_loginfo = ss.str(); // 保存日志头}// 返回临时对象 形成连续留提取template <typename T>LogMessage &operator<<(const T &info) // 重载<<操作符{stringstream ss;ss << info; // 将信息转换为字符串_loginfo += ss.str(); // 追加到日志信息return *this; // 返回当前对象,支持链式调用}~LogMessage() // 析构函数{if (_logger._fflush_strategy) // 检查是否有刷新策略{_logger._fflush_strategy->SyncLog(_loginfo); // 调用刷新策略输出日志}}private:string _curr_time; // 当前时间LogLevel _level; // 日志等级pid_t _pid; // 进程IDstd::string _src_name; // 文件名int _line_number; // 行号std::string _loginfo; // 完整的日志信息Logger &_logger; // 引用日志类对象};// 这里故意写成返回临时对象LogMessage operator()(LogLevel level, std::string name, int line) // 日志类重载()操作符{return LogMessage(level, name, line, *this); // 返回日志消息对象}private:// 指向策略类的智能指针unique_ptr<LogStrategy> _fflush_strategy; // 日志刷新策略对象};// 全局日志对象Logger logger;// 使用宏,简化用户操作,获取文件名和行号#define LOG(level) logger(level, __FILE__, __LINE__) // 定义日志宏#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy() // 启用控制台日志策略宏#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy() // 启用文件日志策略宏
}
#endif



3-1-4 策略模式
策略模式(Strategy
Pattern)是一种行为型设计模式,用于定义一系列的算法,将每个算法封装起来,并使它们可以互换。策略模式让算法的变化独立于使用算法的客户。它提供了一种方法,使得算法可以在运行时动态地切换,而不需要修改客户端代码。
核心概念
-
策略接口(Strategy Interface): 定义了所有支持的操作的公共接口。客户端使用这个接口来调用具体的算法。 -
具体策略(Concrete Strategies): 实现了策略接口的具体算法。每个具体策略类都提供了一种不同的算法实现 -
上下文(Context): 维护一个对策略对象的引用,允许客户端调用策略定义的接口。上下文可以动态地切换策略对象,从而改变其行为。
#include <iostream>
#include <vector>
#include <algorithm>// 策略接口
class SortStrategy
{
public:virtual void sort(std::vector<int> &data) = 0;virtual ~SortStrategy() {}
};
// 冒泡排序策略
class BubbleSortStrategy : public SortStrategy
{
public:void sort(std::vector<int> &data) override{for (size_t i = 0; i < data.size(); ++i){for (size_t j = 0; j < data.size() - i - 1; ++j){if (data[j] > data[j + 1]){std::swap(data[j], data[j + 1]);}}}}
};// 快速排序策略
class QuickSortStrategy : public SortStrategy
{
public:void sort(std::vector<int> &data) override{std::sort(data.begin(), data.end());}
};
// 上下文类
class SortContext
{
public:SortContext(SortStrategy *strategy) : _strategy(strategy) {}void setStrategy(SortStrategy *strategy){_strategy = strategy;}void sort(std::vector<int> &data){_strategy->sort(data);}private:SortStrategy *_strategy;
};
int main()
{std::vector<int> data = {5, 2, 9, 1, 5, 6};// 使用冒泡排序策略SortContext context(new BubbleSortStrategy());context.sort(data);for (int num : data){std::cout << num << " ";}std::cout << std::endl;// 切换到快速排序策略context.setStrategy(new QuickSortStrategy());context.sort(data);for (int num : data){std::cout << num << " ";}std::cout << std::endl;return 0;
}
3-2 线程池设计
3-2-1 线程池:
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络
sockets等的数量。
3-2-2 线程池的应用场景:
-
需要大量的线程来完成任务,且完成任务的时间比较短。比如WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为
Telnet会话时间比线程的创建时间大多了。 -
对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
-
接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。
3-2-3 线程池的种类
- a. 创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中的任务接口
- b. 浮动线程池,其他同上
此处,我们选择固定线程个数的线程池。

3-2-4 ThreadPool.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include "Log.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Mutex.hpp"
using namespace std;namespace ThreadPoolModule
{using namespace ThreadModule;using namespace LogModule;using namespace CondModule;using namespace MutexModule;static const int gnum = 5; // 默认线程池大小template <typename T>class ThreadPool{public:// 初始化线程池ThreadPool(int num = gnum) // 构造函数,初始化线程池: _num(num), _isrunning(false), _sleepernum(0){for (int i = 0; i < _num; i++){_threads.emplace_back( // 创建线程对象[this](){HandlerTask(); // 线程执行的任务处理函数});}}// 往任务队列push任务bool Enqueue(const T &in) // 将任务加入任务队列{if (_isrunning) // 检查线程池是否在运行{LockGuard guard(_mutex); // 加锁,保护任务队列_taskq.push(in); // 将任务加入队列if (_threads.size() == _sleepernum) // 如果所有线程都在休眠{WakeUponce(); // 唤醒一个线程}return true;}return false;}// 启动线程池void Start() // 启动线程池{if (_isrunning) // 如果线程池已经在运行,直接返回{return;}_isrunning = true; // 设置线程池为运行状态for (auto &x : _threads) // 遍历线程池中的线程{x.Start(); // 启动每个线程LOG(LogLevel::INFO) << "start new thread success: " << x.Name(); // 日志记录}}void HandlerTask() // 线程任务处理函数{char name[128];pthread_getname_np(pthread_self(), name, sizeof(name)); // 获取当前线程名称while (true)//while循环防止虚伪唤醒{T t;{LockGuard guard(_mutex); // 加锁,保护任务队列// 如果任务队列为空并且线程池在运行状态,线程进入等待while (_taskq.empty() && _isrunning){_sleepernum++; // 增加休眠线程计数_cond.Wait(_mutex); // 等待条件变量_sleepernum--; // 减少休眠线程计数}// 如果线程池不运行并且任务队列为空,线程退出if (!_isrunning && _taskq.empty()){LOG(LogLevel::INFO) << name << " 退出了, 线程池退出&&任务队列为空";break;}t = _taskq.front(); // 获取任务_taskq.pop(); // 弹出任务}t(); // 执行任务}}static ThreadPool<T> *GetInstance() // 获取线程池单例{if (inc == nullptr) // 检查单例是否已创建{LockGuard lockguard(_lock); // 加锁,防止多线程并发创建LOG(LogLevel::DEBUG) << "获取单例....";if (inc == nullptr) // 双重检查锁定{LOG(LogLevel::DEBUG) << "首次使用单例, 创建之....";inc = new ThreadPool<T>; // 创建线程池单例inc->Start(); // 启动线程池}}return inc;}void WakeUponce() // 唤醒一个休眠线程{_cond.Signal(); // 信号量通知一个线程LOG(LogLevel::INFO) << "唤醒一个休眠线程";}void WakeUpAllThread() // 唤醒所有休眠线程{LockGuard guard(_mutex); // 加锁,保护条件变量if (_sleepernum > 0) // 如果有休眠线程{_cond.Brodcast(); // 广播通知所有线程}LOG(LogLevel::INFO) << "唤醒所有的休眠线程";}void Stop() // 停止线程池{if (!_isrunning) // 如果线程池已经停止,直接返回{return;}_isrunning = false; // 设置线程池为停止状态WakeUpAllThread(); // 唤醒所有线程}void Join() // 等待线程池中的所有线程完成{for (auto x : _threads) // 遍历线程池中的线程{x.Join(); // 等待每个线程完成}}private:std::vector<Thread> _threads; // 线程池,存储线程对象int _num; // 线程池中线程的数量std::queue<T> _taskq; // 任务队列,存储任务Cond _cond; // 条件变量,用于线程同步Mutex _mutex; // 互斥锁,保护任务队列和条件变量bool _isrunning; // 线程池是否在运行int _sleepernum; // 休眠线程的数量static ThreadPool<T> *inc; // 线程池单例指针static Mutex _lock; // 用于单例创建的互斥锁};template <typename T>ThreadPool<T> *ThreadPool<T>::inc = nullptr; // 初始化单例指针template <typename T>Mutex ThreadPool<T>::_lock; // 初始化单例锁
}
线程退出:只有线程池的任务取完或者线程不运行才会退出
也就是说只要还有任务没取完 或者线程正在运行就不退出
但是有可能有线程还在休眠 所以我们应该唤醒所有的休眠线程 把所有的线程设置为退出状态
后言
这就是日志器与线程池设计。大家自己好好消化!今天就分享到这! 感谢各位的耐心垂阅!咱们下期见!拜拜~


