Linux中线程池的简单实现 -- 线程安全的日志模块,策略模式,线程池的封装设计,单例模式,饿汉式单例模式,懒汉式单例模式
目录
1. 对线程池的理解
1.1 基本概念
1.2 工作原理
1.3 线程池的优点
2. 日志与策略模式
2.1 日志认识
2.2 策略模式
2.2.1 策略模式的概念
2.2.2 工作原理
2.2 自定义日志系统的实现
3. 线程池设计
3.1 简单线程池的设计
3.2 线程安全的单例模式线程池的设计
3.2.1 单例模式
3.2.1.1 概念
3.2.1.2 设计原理
3.2.2 饿汉实现方式
3.2.3 懒汉实现方式
1. 对线程池的理解
线程池是一种多线程处理形式,用于管理和复用线程资源,以提高程序的性能和效率。
1.1 基本概念
线程池是一种线程的使用模式,它预先创建一定数量的线程,并将这些线程存储在一个“池”(容器)中,当有新的任务提交时,线程池会从池中取出一个空闲的线程来执行该任务;如果池中没有空闲线程且线程数量未达到最大限制,会创建新的线程来处理任务;若线程数量已达到最大限制,任务会被放入任务队列中等待。任务执行完毕后,线程不会被销毁,而是返回到线程池中,等待下一个任务。
1.2 工作原理
(1)线程池的初始化:在创建线程池时,会根据配置参数创建一定数量的线程,并将它们置于就绪状态,等待任务的到来。
(2)任务提交:当有新的任务需要执行时,将任务添加到任务队列中。
(3)线程分配:线程池中的线程会不断地从任务队列中获取任务。如果队列中有任务,线程会取出任务并执行;如果队列为空,线程会进入等待状态,直到有新的任务加入。
(4)任务执行:线程获取到任务后,会执行任务中的逻辑。任务执行完成后,线程会释放资源,并再次回到线程池中等待下一个任务。
(5)线程池的关闭:当线程池不再需要使用时,可以关闭线程池。关闭过程中,会停止接受新的任务,并等待已有的任务执行完毕,最后销毁所有线程。
1.3 线程池的优点
(1)提高性能:避免了频繁创建和销毁线程带来的开销。线程的创建和销毁是比较昂贵的操作,使用线程池可以复用已有的线程,减少了系统资源的消耗,提高了程序的响应速度。
(2)资源管理:可以对线程的数量进行有效控制,避免因创建过多线程导致系统资源耗尽。通过设置线程池的最大线程数,可以确保系统在高并发情况下仍能稳定运行。
(3)提高响应速度:由于线程池中的线程已经预先创建好,当有任务提交时,可以立即分配线程执行任务,减少了任务的等待时间。
2. 日志与策略模式
2.1 日志认识
计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态,记录异常信息,帮助快速定位问题并支持程序员进行问题修复。是系统维护,故障排除和安全管理的重要工具。
日志有现有的解决方案,如:spdlog,glog,Boost.Log,Log4cxx等等。
下面实现一个自定义类型的日志系统,日志的格式有以下指标:时间,日志等级,进程pid,输出日志的文件,行号,日志内容。
下列是期望输出的日志格式:
2.2 策略模式
2.2.1 策略模式的概念
策略模式定义了一系列算法,将每个算法都封装起来,并且使它们可以互相替换。该模式让算法的变化独立于使用算法的客户,属于行为型模式。通过使用策略模式,可以将算法的选择和实现分离,使得代码更加灵活、可维护和可扩展。
2.2.2 工作原理
(1) 策略接口或抽象类:定义了一个公共的接口或抽象类,用于规范具体策略类的行为。这个接口或抽象类中声明了一个或多个抽象方法,这些方法代表了不同策略可以执行的操作。
(2) 具体策略类:实现了策略接口或继承自抽象策略类,每个具体策略类都实现了特定的算法或行为。它们是策略模式的核心,负责具体的业务逻辑实现。
(3) 上下文类:持有一个策略接口的引用,通过该引用调用具体策略类的方法来执行相应的算法。上下文类可以根据不同的条件或用户需求,动态地切换使用不同的具体策略类。本质上就是多态特性。
2.2 自定义日志系统的实现
由于实现的日志系统要支持多线程程序日志的有序打印,所以不管在访问显示器还是访问文件的时候都需要通过加锁来维护线程之间的互斥关系。
这里对 Linux 系统中的 pthread 库中的互斥锁进行面向对象的封装:
// Mutex.hpp#pragma once
#include <pthread.h>// 将互斥量接口封装成面向对象的形式
namespace MutexModule
{class Mutex{public:Mutex(){int n = pthread_mutex_init(&_mutex, nullptr);(void)n;}~Mutex(){int n = pthread_mutex_destroy(&_mutex);(void)n;}void Lock(){int n = pthread_mutex_lock(&_mutex);(void)n;}void Unlock(){int n = pthread_mutex_unlock(&_mutex);(void)n;}pthread_mutex_t* Get() // 获取原生互斥量的指针{return &_mutex;}private:pthread_mutex_t _mutex;};// 采用RAII风格进行锁管理,当局部临界区代码运行完的时候,局部LockGuard类型的对象自动进行释放,调用析构函数释放锁class LockGuard{public:LockGuard(Mutex &mutex): _mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex& _mutex;};
}
实现的一些细节在下列代码中的注释中有所体现。
// Log.hpp#ifndef __LOG_HPP__
#define __LOG_HPP__#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <ctime>
#include <memory>
#include <unistd.h>
#include "Mutex.hpp"namespace LogModule
{using namespace MutexModule;const std::string gsep = "\r\n";// 策略模式 -- 利用C++的多态特性// 1. 刷新策略 a: 向显示器打印 b: 向文件中写入// 刷新策略基类class LogStrategy{public:virtual ~LogStrategy() = default;virtual void SyncLog(const std::string &message) = 0;};// 显示器打印日志的策略class ConsoleLogStrategy : public LogStrategy{public:ConsoleLogStrategy(){}void SyncLog(const std::string &message) override{// 加锁使多线程原子性的访问显示器LockGuard lockGuard(_mutex);std::cout << message << gsep;}~ConsoleLogStrategy(){}private:Mutex _mutex;};// 文件打印日志策略// 默认的日志文件路径和日志文件名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){// 加锁使多线程原子性的访问文件LockGuard lockGuard(_mutex);// 判断目录是否存在if (std::filesystem::exists(_path)) // 检测文件系统对象(文件,目录,符号链接等)是否存在{return;}try{// 如果目录不存在,递归创建目录std::filesystem::create_directories(_path);}catch (const std::filesystem::filesystem_error &e) // 如果创建失败则打印异常信息{std::cerr << e.what() << '\n';}}void SyncLog(const std::string &message) override{LockGuard lockGuard(_mutex);// 追加方式向文件中写入std::string fileName = _path + (_path.back() == '/' ? "" : "/") + _file;// std::ofstream是C++标准库中用于输出到文件的流类,主要用于将数据写入文件std::ofstream out(fileName, std::ios::app);if (!out.is_open()){return;}out << message << gsep;out.close();}~FileLogStrategy(){}private:std::string _path; // 日志文件所在路径std::string _file; // 日志文件本身Mutex _mutex;};// 2. 形成完整日志并刷新到指定位置// 2.1 日志等级enum class LogLevel{DEBUG,INFO,WARNING,ERROR,FATAL};// 2.2 枚举类型的日志等级转换为字符串类型std::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";}}// 2.3 获取当前时间的函数std::string GetCurTime(){// time 函数参数为一个time_t类型的指针,若该指针不为NULL,会把获取到的当前时间值存储在指针指向的对象中// 若传入为NULL,则仅返回当前时间,返回从1970年1月1日0点到目前的秒数time_t cur = time(nullptr);struct tm curTm;// localtime_r是localtime的可重入版本,主要用于将time_t类型表示的时间转换为本地时间,存储在struct tm 结构体中localtime_r(&cur, &curTm);char timeBuffer[128];snprintf(timeBuffer, sizeof(timeBuffer), "%4d-%02d-%02d %02d:%02d:%02d",curTm.tm_year + 1900,curTm.tm_mon + 1,curTm.tm_mday,curTm.tm_hour,curTm.tm_min,curTm.tm_sec);return timeBuffer;}// 2.4 日志形成并刷新class Logger{public:// 默认刷新到显示器上Logger(){EnableConsoleLogStrategy();}void EnableConsoleLogStrategy(){// std::make_unique用于创建并返回一个std::unique_ptr对象_fflushStrategy = std::make_unique<ConsoleLogStrategy>();}void EnableFileLogStrategy(){_fflushStrategy = std::make_unique<FileLogStrategy>();}// 内部类默认是外部类的友元类,可以访问外部类的私有成员变量// 内部类LogMessage,表示一条日志信息的类class LogMessage{public:LogMessage(LogLevel &level, std::string &srcName, int lineNum, Logger &logger): _curTime(GetCurTime()),_level(level),_pid(getpid()),_srcName(srcName),_lineNum(lineNum),_logger(logger){// 日志的基本信息合并起来// std::stringstream用于在内存中进行字符串的输入输出操作, 提供一种方便的方式处理字符串// 将不同类型的数据转换为字符串,也可以将字符串解析为不同类型的数据std::stringstream ss;ss << "[" << _curTime << "] "<< "[" << Level2Str(_level) << "] "<< "[" << _pid << "] "<< "[" << _srcName << "] "<< "[" << _lineNum << "] "<< "- ";_logInfo = ss.str();}// 使用模板重载运算符<< -- 支持不同数据类型的输出运算符重载template <typename T>LogMessage &operator<<(const T &info){std::stringstream ss;ss << info;_logInfo += ss.str();return *this;}~LogMessage(){if (_logger._fflushStrategy){_logger._fflushStrategy->SyncLog(_logInfo);}}private:std::string _curTime; // 日志时间LogLevel _level; // 日志等级pid_t _pid; // 进程pidstd::string _srcName; // 输出日志的文件名int _lineNum; //输出日志的行号std::string _logInfo; //完整日志内容Logger &_logger; // 方便使用策略进行刷新};// 使用宏进行替换之后调用的形式如下// logger(level, __FILE__, __LINE__) << "hello world" << 3.14;// 这里使用仿函数的形式,调用LogMessage的构造函数,构造一个匿名的LogMessage对象// 返回的LogMessage对象是一个临时对象,它的生命周期从创建开始到包含它的完整表达式结束(可以简单理解为包含// 这个对象的该行代码)// 代码调用结束的时候,如果没有LogMessage对象进行临时对象的接收,则会调用析构函数,// 如果有LogMessage对象进行临时对象的接收,会调用拷贝构造或者移动构造构造一个对象,并析构临时对象// 所以通过临时变量调用析构函数进行日志的打印LogMessage operator()(LogLevel level, std::string name, int line){return LogMessage(level, name, line, *this);}~Logger(){}private:std::unique_ptr<LogStrategy> _fflushStrategy;};// 定义一个全局的Logger对象Logger logger;// 使用宏定义,简化用户操作并且获取文件名和行号#define LOG(level) logger(level, __FILE__, __LINE__) // 使用仿函数的方式进行调用#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}#endif
通过下列方式进行调用
#include "Log.hpp"using namespace LogModule;int main()
{Enable_Console_Log_Strategy();LOG(LogLevel::DEBUG) << "debug";LOG(LogLevel::INFO) << "info";LOG(LogLevel::WARNING) << "warning";LOG(LogLevel::ERROR) << "error";LOG(LogLevel::FATAL) << "fatal";Enable_File_Log_Strategy();LOG(LogLevel::DEBUG) << "hello world";LOG(LogLevel::DEBUG) << "hello world";LOG(LogLevel::DEBUG) << "hello world";return 0;
}
向显示器上打印的部分:
向 my.log 文件中打印的部分
3. 线程池设计
3.1 简单线程池的设计
这里模拟创建一个固定线程数量的线程池,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中的任务。下列线程,日志,条件变量,互斥量等的封装参考前面的博客。
// ThreadPool.hpp#pragma once#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include "Log.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Mutex.hpp"namespace ThreadPoolModule
{using namespace ThreadModule;using namespace LogModule;using namespace CondModule;using namespace MutexModule;static const int gnum = 5; // 使用全局变量来表示一个线程池默认的线程数量template <typename T> // 使用模版的方式使线程池支持多类型的任务class ThreadPool{private:void WakeUpAllThread(){if (_sleep_num)_cond.Broadcast();LOG(LogLevel::DEBUG) << "唤醒所有休眠线程";}void WakeOne(){_cond.Signal();LOG(LogLevel::INFO) << "唤醒一个休眠的线程";}public:// 创建固定数量的线程ThreadPool(int num = gnum): _num(num),_isrunning(false),_sleep_num(0){for (int i = 0; i < _num; i++){_threads.emplace_back([this](){ HandlerTask(); }); // 调用线程的构造函数,线程的构造函数形参是一个回调函数}}void HandlerTask(){char name[64];pthread_getname_np(pthread_self(), name, sizeof(name));while (true){T t;// 处理任务{LockGuard lockGuard(_mutex);// 1. 队列为空,线程池没有退出,进行休眠while (_taskq.empty() && _isrunning){_sleep_num++;LOG(LogLevel::INFO) << name << " 进入休眠";_cond.Wait(_mutex);_sleep_num--;}// 2. 任务为空,线程池退出,则该线程退出if (!_isrunning && _taskq.empty()){LOG(LogLevel::INFO) << name << " 退出, 因为线程池退出&&任务队列为空";break;}// 3. 获取任务t = _taskq.front();_taskq.pop();}t(); // 4. 处理任务// LOG(LogLevel::DEBUG) << name << " is running";}}bool Enqueue(const T &in){if (_isrunning) // 如果线程池停止,则停止入任务{LockGuard lockGuard(_mutex);_taskq.push(in);if (_threads.size() == _sleep_num) // 如果全部线程都在休眠,则唤醒一个线程WakeOne();return true;}return false;}void Start(){if (_isrunning)return;_isrunning = true;for (auto &thread : _threads){thread.Start();}}void Stop(){// 1. 将运行标志位置为falseLockGuard lockGuard(_mutex);if (!_isrunning)return;_isrunning = false;// 2. 唤醒休眠的线程,然后再HandlerTask中进行退出WakeUpAllThread();}void Join(){for (auto &thread : _threads){thread.Join();LOG(LogLevel::INFO) << thread.GetName() << " 被Join";}}~ThreadPool(){}private:std::vector<Thread> _threads;int _num; // 线程数量std::queue<T> _taskq; // 任务队列Cond _cond;Mutex _mutex;bool _isrunning;int _sleep_num;};
}
//Task.hpp#pragma once#include <iostream>
#include <functional>
#include <unistd.h>
#include "Log.hpp"using namespace LogModule;
using task_t = std::function<void()>;void Download()
{LOG(LogLevel::DEBUG) << "I am a download task...";// sleep(3);
}
// Main.cc#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"using namespace LogModule;
using namespace ThreadPoolModule;
int main()
{Enable_Console_Log_Strategy();ThreadPool<task_t> *tp = new ThreadPool<task_t>();tp->Start();sleep(5);int count = 10;while (count){tp->Enqueue(Download);sleep(1);count--;}tp->Stop();tp->Join();return 0;
}
在主程序中,主线程循环向任务队列中放入下载任务,然后唤醒线程池中休眠的线程进行任务处理。在处理完之后停止线程池,然后对线程池中的线程进行回收。
这里因为任务处理的过程时间很短,所以在下一个任务进入的时候,上一个处理任务的线程处理完任务就进行了休眠,导致打印出来的日志是唤醒一个线程,处理一个任务。当处理任务的时间消耗较长时,线程进入休眠的概率就会下降。
3.2 线程安全的单例模式线程池的设计
3.2.1 单例模式
3.2.1.1 概念
单例模式确保一个类只有一个实例存在,同时提供一个全局访问点,让其他代码可以方便地获取到这个唯一实例。在很多情况下,我们只需要一个实例来协调系统中的各项操作,比如配置文件管理、数据库连接池、日志记录器等,使用单例模式可以避免多个实例带来的资源浪费和数据不一致问题。
3.2.1.2 设计原理
(1)私有构造函数:单例类的构造函数被设置为私有,防止了外部代码通过 new
关键字来创建该类的实例。
(2)静态实例变量:在单例类内部定义一个静态的实例变量,用于保存该类的唯一实例。
(3)静态访问方法:提供一个静态的公共方法,用于获取该类的唯一实例。在这个方法中,会检查实例是否已经创建,如果未创建则创建一个新实例,若已创建则直接返回该实例。
3.2.2 饿汉实现方式
饿汉式单例在类加载时就创建了实例,因此它是线程安全的。但是如果这个实例在程序运行中没有进行使用,就会造成资源的浪费。
下列的实现增加了一个 _stop_flag 标记位,在调用 Stop() 时置为1,防止后续在调用其他函数获取单例的时候,调用 Start() 函数重新将 _isrunning 标记位置为 true。在 Start() 中对_stop_flag 标记位进行判断,如果为真,则再次获取单例的时候,不在将 _isrunning 标记位置为true.
// 饿汉式单例模式线程池
#pragma once#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include "Log.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Mutex.hpp"namespace ThreadPoolModule
{using namespace ThreadModule;using namespace LogModule;using namespace CondModule;using namespace MutexModule;static const int gnum = 5; // 使用全局变量来表示一个线程池默认的线程数量template <typename T> // 使用模版的方式使线程池支持多类型的任务class ThreadPool{private:void WakeUpAllThread(){if (_sleep_num)_cond.Broadcast();LOG(LogLevel::DEBUG) << "唤醒所有休眠线程";}void WakeOne(){_cond.Signal();LOG(LogLevel::INFO) << "唤醒一个休眠的线程";}// 私有化构造函数ThreadPool(int num = gnum) : _num(num),_isrunning(false),_sleep_num(0),_stop_flag(false){for (int i = 0; i < _num; i++){_threads.emplace_back([this](){ HandlerTask(); }); // 调用线程的构造函数,线程的构造函数形参是一个回调函数}}void Start(){if (_isrunning)return;if (_stop_flag)return;_isrunning = true;for (auto &thread : _threads){thread.Start();}}// 禁用拷贝构造和赋值运算符ThreadPool(const ThreadPool<T> &) = delete;ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;public:static ThreadPool<T> *GetInstance(){inc.Start();return &inc;}void HandlerTask(){char name[64];pthread_getname_np(pthread_self(), name, sizeof(name));while (true){T t;// 处理任务{LockGuard lockGuard(_mutex);// 1. 队列为空,线程池没有退出,进行休眠while (_taskq.empty() && _isrunning){_sleep_num++;LOG(LogLevel::INFO) << name << " 进入休眠";_cond.Wait(_mutex);_sleep_num--;}// 2. 任务为空,线程池退出,则该线程退出if (!_isrunning && _taskq.empty()){LOG(LogLevel::INFO) << name << " 退出, 因为线程池退出&&任务队列为空";break;}// 3. 获取任务t = _taskq.front();_taskq.pop();}t(); // 4. 处理任务// LOG(LogLevel::DEBUG) << name << " is running";}}bool Enqueue(const T &in){if (_isrunning) // 如果线程池停止,则停止入任务{LockGuard lockGuard(_mutex);_taskq.push(in);if (_threads.size() == _sleep_num) // 如果全部线程都在休眠,则唤醒一个线程WakeOne();return true;}return false;}void Stop(){// 1. 将运行标志位置为falseLockGuard lockGuard(_mutex);if (!_isrunning)return;_isrunning = false;_stop_flag = true;// 2. 唤醒休眠的线程,然后再HandlerTask中进行退出WakeUpAllThread();}void Join(){for (auto &thread : _threads){thread.Join();LOG(LogLevel::INFO) << thread.GetName() << " 被Join";}}~ThreadPool(){}private:std::vector<Thread> _threads;int _num; // 线程数量std::queue<T> _taskq; // 任务队列Cond _cond;Mutex _mutex;bool _isrunning;int _sleep_num;int _stop_flag;static ThreadPool<T> inc; // 单例};template<typename T>ThreadPool<T> ThreadPool<T>::inc; // 静态成员变量需要在类外进行初始化
}
3.2.3 懒汉实现方式
懒汉式单例在第一次调用静态访问方法时才创建实例,避免了不必要的资源浪费。不过在多线程环境下,这种实现方式不是线程安全的,可能会创建多个实例。需要通过使用互斥量确保在多线程环境下只会创建一个实例。懒汉方式最核心的思想是“延时加载”,从而能够优化服务器的启动速度。
在类中增加一个静态互斥量,并在获取单例的时候进行双层判断,保证在多线程场景下使用单例模式的线程池时不会创建多个线程池单例。
// 懒汉式单例模式线程池#pragma once#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include "Log.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Mutex.hpp"namespace ThreadPoolModule
{using namespace ThreadModule;using namespace LogModule;using namespace CondModule;using namespace MutexModule;static const int gnum = 5; // 使用全局变量来表示一个线程池默认的线程数量template <typename T> // 使用模版的方式使线程池支持多类型的任务class ThreadPool{private:void WakeUpAllThread(){if (_sleep_num)_cond.Broadcast();LOG(LogLevel::DEBUG) << "唤醒所有休眠线程";}void WakeOne(){_cond.Signal();LOG(LogLevel::INFO) << "唤醒一个休眠的线程";}// 私有化构造函数ThreadPool(int num = gnum): _num(num),_isrunning(false),_sleep_num(0){for (int i = 0; i < _num; i++){_threads.emplace_back([this](){ HandlerTask(); }); // 调用线程的构造函数,线程的构造函数形参是一个回调函数}}void Start(){if (_isrunning)return;_isrunning = true;for (auto &thread : _threads){thread.Start();}}// 禁用拷贝构造和赋值运算符ThreadPool(const ThreadPool<T> &) = delete;ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;public:static ThreadPool<T> *GetInstance(){if (inc == nullptr) // 第一次创建的时候需要加锁,保证创建是原子性的{LockGuard lockGuard(_gmutex);if (inc == nullptr) // 双层判断,保证只会创建一个单例{LOG(LogLevel::DEBUG) << "首次使用, 创建单例...";inc = new ThreadPool<T>();inc->Start();}}return inc;}void HandlerTask(){char name[64];pthread_getname_np(pthread_self(), name, sizeof(name));while (true){T t;// 处理任务{LockGuard lockGuard(_mutex);// 1. 队列为空,线程池没有退出,进行休眠while (_taskq.empty() && _isrunning){_sleep_num++;LOG(LogLevel::INFO) << name << " 进入休眠";_cond.Wait(_mutex);_sleep_num--;}// 2. 任务为空,线程池退出,则该线程退出if (!_isrunning && _taskq.empty()){LOG(LogLevel::INFO) << name << " 退出, 因为线程池退出&&任务队列为空";break;}// 3. 获取任务t = _taskq.front();_taskq.pop();}t(); // 4. 处理任务// LOG(LogLevel::DEBUG) << name << " is running";}}bool Enqueue(const T &in){if (_isrunning) // 如果线程池停止,则停止入任务{LockGuard lockGuard(_mutex);_taskq.push(in);if (_threads.size() == _sleep_num) // 如果全部线程都在休眠,则唤醒一个线程WakeOne();return true;}return false;}void Stop(){// 1. 将运行标志位置为falseLockGuard lockGuard(_mutex);if (!_isrunning)return;_isrunning = false;// 2. 唤醒休眠的线程,然后再HandlerTask中进行退出WakeUpAllThread();}void Join(){for (auto &thread : _threads){thread.Join();LOG(LogLevel::INFO) << thread.GetName() << " 被Join";}}~ThreadPool(){}private:std::vector<Thread> _threads;int _num; // 线程数量std::queue<T> _taskq; // 任务队列Cond _cond;Mutex _mutex;bool _isrunning;int _sleep_num;static ThreadPool<T> *inc; // 单例指针static Mutex _gmutex; // 用于多线程场景下保护单例不被多次创建};template <typename T>ThreadPool<T> *ThreadPool<T>::inc = nullptr; // 静态成员变量需要在类外进行初始化template <typename T>Mutex ThreadPool<T>::_gmutex; // 自动调用Mutex的构造函数进行初始化
}
相关文章:
Linux中线程池的简单实现 -- 线程安全的日志模块,策略模式,线程池的封装设计,单例模式,饿汉式单例模式,懒汉式单例模式
目录 1. 对线程池的理解 1.1 基本概念 1.2 工作原理 1.3 线程池的优点 2. 日志与策略模式 2.1 日志认识 2.2 策略模式 2.2.1 策略模式的概念 2.2.2 工作原理 2.2 自定义日志系统的实现 3. 线程池设计 3.1 简单线程池的设计 3.2 线程安全的单例模式线程池的设计 3…...
【Web API系列】深入解析 Web Service Worker 中的 WindowClient 接口:原理、实践与进阶应用
前言 在现代 Web 开发领域中,Service Worker 技术已成为构建离线优先应用和实现高级缓存策略的核心支柱。作为 Service Worker API 体系中的重要组成部分,WindowClient 接口为开发者提供了对受控客户端窗口的精准控制能力。本文将从实际工程实践的角度出…...
哈希封装unordered_map和unordered_set的模拟实现
文章目录 (一)认识unordered_map和unordered_set(二)模拟实现unordered_map和unordered_set2.1 实现出复用哈希表的框架2.2 迭代器iterator的实现思路分析2.3 unordered_map支持[] (三)结束语 (…...
智诚科技苏州SOLIDWORKS授权代理商的卓越之选
在当今数字化转型浪潮中,SOLIDWORKS软件以其强大的功能和广泛的行业应用,成为企业迈向智能制造的有力工具。它不仅提供直观的3D建模环境,帮助企业设计师快速创建精准的3D模型,还涵盖了从概念设计到详细设计、从样品制作到最终产品…...
【网络原理】从零开始深入理解TCP的各项特性和机制.(二)
本篇博客给大家带来的是TCP/IP原理的知识点,重点以TCP为主,接续上篇. 🐎文章专栏: JavaEE初阶 🚀若有问题 评论区见 ❤ 欢迎大家点赞 评论 收藏 分享 如果你不知道分享给谁,那就分享给薯条. 你们的支持是我不断创作的动力 . 王子,公主请阅🚀 …...
51单片机所有寄存器介绍
51单片机所有寄存器介绍 作者将狼才鲸创建日期2025-04-27 参考资料:Intel官方《MCS-51 Programmer’s Guide and Instruction Set.pdf》CSDN阅读地址:51单片机所有寄存器介绍 一、前言 51单片机的寄存器和ARM不一样,有自己专有的名称&…...
4.27算法题
力扣649.Dota2 参议院 649. Dota2 参议院 Dota2 的世界里有两个阵营:Radiant(天辉)和 Dire(夜魇) Dota2 参议院由来自两派的参议员组成。现在参议院希望对一个 Dota2 游戏里的改变作出决定。他们以一个基于轮为过程…...
衡石科技:HENGSHI SENSE 数据权限解决方案
编写目的 本方案主要讲述 HENGSHI SENSE 的数据权限方案,即在 HENGSHI SENSE 系统中,通过同步企业内部的人员属性和组织架构等信息,实现企业内部的每一个用户对于业务数据的读取权限。 本方案的的预期读者为:HENGSHI SENSE 的…...
矩阵系统源码搭建热门音乐功能板块开发,支持OEM
在数字音乐蓬勃发展的当下,矩阵系统中的热门音乐功能板块成为吸引用户的重要部分。它不仅能为用户推荐当下流行的音乐,还能提升用户在系统中的活跃度和留存率。本文将通过详细的源码搭建过程,带你了解如何在矩阵系统中实现一个功能完备的热门…...
深入理解Android Activity生命周期
引言 在Android开发中,理解Activity的生命周期对于创建高效、稳定的应用程序至关重要。无论你是初学者还是资深开发者,掌握Activity生命周期的概念都能帮助你更好地管理资源、优化性能以及处理各种用户交互场景。本文将详细介绍Activity生命周期中的各个事件,并通过示例代码…...
【WEB3】web3.0是什么
互联网在不断发展。 我们即将翻开新的篇章,迎来翻天覆地的变化。 — Web 1.0 只能阅读信息。 它主要是供我们访问和阅读信息,只有极少数人可以真正发布内容。 — Web 2.0,即互联网目前所处的阶段,我们能够在网络上发布内容、建立…...
2025上海车展 | 移远通信重磅发布AR脚踢毫米波雷达,重新定义“无接触交互”尾门
4月25日,在2025上海国际汽车工业展览会期间,全球领先的物联网和车联网整体解决方案供应商移远通信宣布,其全新AR脚踢毫米波雷达RD7702AC正式发布。 该产品专为汽车尾门“无接触交互”设计,基于先进的毫米波技术,融合AR…...
ubuntu安装git及使用(本地git)
ubuntu安装git及使用教程(本地git) 1.ubuntu安装git1.1 查看自己的Ubuntu是否已经装有git1.2 下面进行介绍如何Ubuntu终端安装git (若已安装则可忽略) 2. 配置Git基本信息2.1 若不清楚是否配置的可使用如下命令查看2.2 未配置用户…...
数智读书笔记系列031《HIS内核设计之道——医院信息系统规划设计系统思维》书籍简介与读书笔记
一、作者与出版信息 作者团队(核心贡献者) 任连仲 身份:中国工程院院士(2022年当选),解放军总医院信息科原主任技术贡献: 主导“军字一号”系统架构设计(1997-2005年),支撑全国300余家三甲医院信息化建设提出“医疗数据语义网格”理论,获国家科技进步二等奖(2018年…...
WinForm真入门(18)——DateTimePicker控件解析
一、基本概念 DateTimePicker 是 Windows 窗体中用于选择日期和时间的控件,支持以下交互方式: 通过下拉日历选择日期通过上下按钮调整时间直接输入日期或时间 适用于需要规范日期格式、限制日期范围或快速输入的场景(如预约系统、数据…...
关于堆栈指针的那些事 | bootloader 如何跳转app
问题描述 堆栈指针的值通常存储在 App 的向量表(Vector Table)的第一个位置(0x08002000),为什么? 在嵌入式系统中,堆栈指针(SP)的值存储在应用程序(App&…...
如何在 iPhone 上恢复已删除的联系人:简短指南
从 iPhone 中删除联系人相当容易,但如果您不小心删除了错误的联系人或丢失了所有联系人怎么办?这可能是任何智能手机用户都可能发生的最糟糕的噩梦之一。 如何在 iPhone 上恢复已删除的联系人 我个人在我的列表上看到几个用户发布关于他们如何丢失所有联…...
使用Aspose.Words将Word转换为HTML时,字体样式丢失问题及解决方法
使用Aspose.Words将Word转换为HTML时,字体样式丢失问题及解决方法 引言 ✨一、问题描述 📉二、问题分析 🔍三、解决方案 🛠️四、总结 🏁 引言 ✨ 在实际开发中,使用Aspose.Words将Word文档转换为HTML格式…...
更快的图像局部修改与可控生成:Flex.2-preview
Flex.2-preview 文本生成图像扩散模型介绍 一、模型简介 Flex.2-preview 是一种 开源的 80 亿参数文本生成图像扩散模型,具备通用控制和修复支持功能,是 Flex.1alpha 的下一代版本。该模型由社区开发并为社区服务,采用 Apache 2.0 许可证&a…...
汽车制造行业如何在数字化转型中抓住机遇?
近年来,随着新一轮科技革命和产业变革的深入推进,汽车制造行业正迎来一场前所未有的数字化转型浪潮。无论是传统车企还是新势力品牌,都在积极探索如何通过数字化技术提升竞争力、开拓新市场。那么,在这场变革中,汽车制…...
数据可视化 —— 直方图
一、前言 直方图(Histogram)是一种用柱状图形表示数据分布的统计图表,它将数据划分为连续的区间(称为“分箱”或“区间”),统计每个区间内的数据频数(或频率),并用柱形的…...
1、Linux操作系统下,ubuntu22.04版本切换中英文界面
切换中英文界面的方法很多,我也是按照一个能用的方法弄过来并且记录, 1.如果刚开始使用Ubuntu环境,桌面的语言环境为英文,需要安装中文简体的字体包 打开桌面终端,输入 sudo apt install language-pack-zh-hans lan…...
《MySQL 技术内幕-innoDB 存储引擎》笔记
💡 根据 遗忘曲线:如果没有记录和回顾,6天后便会忘记75%的内容 读书笔记正是帮助你记录和回顾的工具,不必拘泥于形式,其核心是:记录、翻看、思考::: 书名MySQL 技术内幕-innoDB 存储引擎作者姜承尧状态已读…...
C++ AVL树的实现
在上一篇博客我们学习了二叉搜索树的实现,现在我们开始手动实现AVL树。 二叉搜索树-CSDN博客 1.AVL树的概念 AVL树是最先发明的⾃平衡⼆叉查找树,AVL是⼀颗空树,或者具备下列性质的⼆叉搜索树:它的左右⼦树都是AVL树,…...
多视觉编码器协同与高低分辨率特征融合技术综述
本文主要介绍(论文发表时间:24.03-25.01)在多模态中使用多个视觉编码器如何进行特征融合操作(之所以用多视觉编码器,主要用途在于:有些视觉编码器可能只能提取到部分信息,就想通过另外一个编码器…...
力扣4-最长公共前缀
一.题目 编写一个函数来查找字符串数组中的最长公共前缀。 如果不存在公共前缀,返回空字符串 ""。 示例 1: 输入:strs ["flower","flow","flight"] 输出:"fl"示例 2&…...
贪心算法-860.柠檬水找零-力扣(LeetCode)
一、题目解析 我们需要注意我们是没有初始零钱的,所以当第一个顾客支付10或20时,无法找零此时返回false。 二、算法解析 根据贪心算法的解决方式,我们需要先把解决该问题分解为若干步。 首先对于顾客支付的钱共有三种,5…...
Kubernetes学习笔记-配置Service对接第三方访问
在Kubernetes中配置Service对接第三方访问,可以选择以下方案实现: ExternalName Service(基于DNS别名) 适用场景:外部服务必须有固定域名Service配置文件如下: apiVersion: v1 kind: Service metadata…...
pikachu靶场-敏感信息泄露
一、敏感信息泄露的危害 1. 个人隐私与数据安全 身份盗窃:泄露个人身份信息(如姓名、身份证号、手机号)可被用于诈骗、冒名开户等犯罪活动。账户劫持:暴露用户账号密码、邮箱等凭证,导致社交媒体、银行账户被非法登录。…...
ppt章节页怎么做好看?ppt章节页模板
ppt章节页怎么做好看?ppt章节页怎么排版?ppt章节页模板: PPT章节_模板素材_PPT模板_ppt素材_免抠图片_AiPPTer...
ubuntu扩展逻辑卷并调整文件系统大小步骤
安装好ubuntu如果没有调整磁盘空间,一般默认给你100G的空间,在用完时再调整也还来得及,下面是 ubuntu扩展逻辑卷并调整文件系统大小步骤: 1. 扩展逻辑卷 运行以下命令来扩展逻辑卷 /dev/ubuntu-vg/ubuntu-lv,使其使用卷组中所有未分配的空间ÿ…...
2.脚本文件初识
—>1.Makefile—自动化构建和管理项目的文件见这篇<— 1.编程语言 编程语言分为2类,一类是编译型语言,将源文件经过编译得到可执行文件,该执行文件可以在特定平台上运行,其他平台则不行,因此是不跨平台的编程语…...
FastAPI + Redis Pub/Sub + WebSocket 组合解决方案的详细介绍
以下是对 FastAPI Redis Pub/Sub WebSocket 组合解决方案的详细介绍,涵盖技术原理、实现步骤、协作流程和适用场景。 1. 技术概述 1.1 FastAPI 特性:基于 Python 的现代异步框架,支持 async/await,性能高效,适合高…...
泛型的诗意——深入C++模板的艺术与科学(模版进阶)
前言: 在之前,小编讲述了模版的初阶内容,当时小编讲述了模版的书写,方便之后容器的讲解以及模拟实现,现在小编已经带领各位学习了很多容器,模版初阶的知识已经用的很多了,今天小编讲述一下全新的…...
【极致版】华为云Astro轻应用抽取IoTDA影子设备参数生成表格页面全流程
做份极致详细Astro调取iotda影子设备数据的操作手册,每一步都分成: 要进入哪个界面 点哪个按钮 要填什么内容(样例) 如果出错怎么办 填写示例 完全对应你这个需求:Astro轻应用抽取IoTDA影子设备数据,…...
业务中台与数据中台:企业数字化转型的核心引擎
前言:在当今数字化浪潮下,企业为了提升运营效率、加速创新步伐并更好地适应市场变化,业务中台与数据中台应运而生,成为企业架构中的关键组成部分。本文将深入探讨业务中台和数据中台的简介、发展史、技术流环节以及在实际生产中的…...
前端分页与瀑布流最佳实践笔记 - React Antd 版
前端分页与瀑布流最佳实践笔记 - React Antd 版 1. 分页与瀑布流对比 分页(Pagination)瀑布流(Infinite Scroll)展示方式按页分批加载,有明确页码控件滚动到底部时自动加载更多内容,无明显分页用户控制用…...
【网络原理】从零开始深入理解TCP的各项特性和机制.(三)
上篇介绍了网络原理传输层TCP协议的知识,本篇博客给大家带来的是网络原理剩余的内容, 总体来说,这部分内容没有上两篇文章那么重要,本篇知识有一个印象即可. 🐎文章专栏: JavaEE初阶 🚀若有问题 评论区见 ❤ 欢迎大家点赞 评论 收藏 分享 如果你不知道分…...
MySQL:13.用户管理
13. 用户管理 如果我们只能使用root用户,这样存在安全隐患。这时,就需要使用MySQL的用户管理。 13.1 用户 13.1.1 用户信息 MySQL中的用户,都存储在系统数据库mysql的user表中 mysql> use mysql; Database changed mysql> select h…...
leetcode0103. 二叉树的锯齿形层序遍历-medium
1 题目:二叉树的锯齿形层序遍历 官方标定难度:中 给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行…...
【Go语言】ORM(对象关系映射)库
github.com/jinzhu/gorm 是 Go 语言中一个非常流行的 ORM(对象关系映射)库,用于简化与关系型数据库的交互。以下是关于它的关键信息: 核心特点 全功能 ORM 支持主流数据库:MySQL、PostgreSQL、SQLite、SQL Server 等。…...
Java : GUI
AWT 初始化界面 直接封装起来: panel 的添加 布局 流式布局,控制按钮的位置 东西南北中布局 网格布局 frame.pack();java函数,会自动选择最优的布局 事件监听 给按钮添加 添加文本 画笔 鼠标监听 键盘监听 JDialog”弹窗 默认有关闭事件 标签&#…...
ipa包安装到apple手机上
获ipa包的方式 ipatool 下载appStore的ipa包-CSDN博客 方式一:巨魔商店 原理是利用apple的漏洞,但是有低版本的系统要求 TrollStore - Always Sideload Any IPAs For FreeTrollStore - The ultimate jailbreak app for iOS. Permanently install any …...
JavaScript输出数据的方法
1. console.log() console.log()是最常用的方法之一,用于在浏览器的控制台(Console)中输出信息。这对于调试和查看变量的值非常有用。 console.log("Hello, world!");2. alert() alert()方法会弹出一个带有指定消息和确定按钮的警告…...
操作系统:计算机世界的基石与演进
一、操作系统的本质与核心功能 操作系统如同计算机系统的"总管家",在硬件与应用之间架起关键桥梁。从不同视角观察,其核心功能呈现多维价值: 硬件视角的双重使命: 硬件管理者:通过内存管理、进程调度和设…...
FFmpeg之三 录制音频并保存, API编解码从理论到实战
在学习FFmpeg的时候,想拿demo来练习,官方虽有示例,但更像是工具演示,新手不好掌握,在网上找不到有文章,能给出完整的示例和关键点的分析说明,一步一个错误,慢慢啃过来的,…...
幂等性处理解决方案实战示例
幂等性处理解决方案实战示例 幂等性是指对同一个操作执行一次或多次,产生的结果是相同的。在分布式系统、网络请求和金融交易等场景中,幂等性设计至关重要。下面我将介绍几种常见的幂等性处理方案及其实战示例。 1. 唯一标识符方案 原理:为…...
华为仓颉编程语言的实际用法与使用领域详解
华为仓颉编程语言的实际用法与使用领域详解 一、语言概述与核心特性 华为仓颉编程语言是面向万物智联时代的系统级编程语言,其核心特性包括: 三重内存安全机制:所有权系统 + 引用检查 + 硬件辅助防护零成本抽象:高级语法不牺牲底层性能全场景支持:从嵌入式设备到量子计算…...
JavaEE-多线程实战01
Java 多线程入门:第一个多线程程序 在 Java 中,多线程编程是非常重要的一部分。本篇文章将通过示例,带你快速了解如何创建第一个多线程程序,并深入分析其运行机制。 1. 创建一个线程类并继承 Thread 在 Java 中,我们…...
当AI浏览器和AI搜索替代掉传统搜索份额时,老牌的搜索引擎市场何去何从。
AI搜索与传统搜索优劣势分析 AI搜索优势 理解和处理查询方式更智能:利用自然语言处理(NLP)和机器学习技术,能够更好地理解用户的意图和上下文,处理复杂的问答、长尾问题以及多轮对话,提供更为精准和相关的…...