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

Linux_详解线程池

18fde01fee5e4278981004762ce48cc4.png

✨✨ 欢迎大家来到小伞的大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:LInux_st
小伞的主页:xiaosan_blog

制作不易!点个赞吧!!谢谢喵!!

1. 线程池

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

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

1.1 日志与策略模式

什么是设计模式

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

日志格式以下几个指标是必须得有的

  • 时间戳
  • 日志等级
  • 日志内容

以下几个指标是可选的

  • 文件名行号
  • 进程,线程相关id信息等

日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx等等,我们依旧采用自定义日志的方式。

这里我们采用设计模式-策略模式来进行日志的设计,具体策略模式介绍,详情看代码。
我们想要的日志格式如下:

可读性很好的时间][日志等级】[进程pid][打印对应日志的文件名][行号]-消息内容,支持可
变参数
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [17] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [2i] - hello world
[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world
Log.hpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <memory>
#include <ctime>
#include <sstream>
#include <filesystem> // C++17, 需要⾼版本编译器和-std=c++17
#include <unistd.h>
#include "Lock.hpp"
namespace LogModule
{using namespace LockModule; // 使⽤我们⾃⼰封装的锁,也可以采⽤C++11的锁.// 默认路径和⽇志名称const std::string defaultpath = "./log/";const std::string defaultname = "log.txt";// ⽇志等级enum class LogLevel{DEBUG,INFO,WARNING,ERROR,FATAL};// ⽇志转换成为字符串std::string LogLevelToString(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 GetCurrTime(){time_t tm = time(nullptr);struct tm curr;localtime_r(&tm, &curr);// 这⾥如果不好看,可以考虑sprintf// ⽅法 1// std::stringstream ss;// ss << curr.tm_year + 1900 << "-" << curr.tm_mon << "-" <<curr.tm_mday << " "// << curr.tm_hour << ":" << curr.tm_min << ":" << curr.tm_sec;// return ss.str();// ⽅法 2char timebuffer[64];snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d% 02d : % 02d : % 02d ", curr.tm_year + 1900,curr.tm_mon,curr.tm_mday,curr.tm_hour,curr.tm_min,curr.tm_sec);return timebuffer;}// 策略模式,策略接⼝ class LogStrategy{public:virtual ~LogStrategy() = default;                     // 策略的构造函数virtual void SyncLog(const std::string &message) = 0; // 不同模式核⼼是刷新⽅式的不同};// 控制台⽇志策略,就是⽇志只向显⽰器打印,⽅便我们debugclass ConsoleLogStrategy : public LogStrategy{public:void SyncLog(const std::string &message) override{LockGuard LockGuard(_mutex);std::cerr << message << std::endl;}~ConsoleLogStrategy(){// std::cout << "~ConsoleLogStrategy" << std::endl; // for debug}private:Mutex _mutex; // 显⽰器也是临界资源,保证输出线程安全};// ⽂件⽇志策略class FileLogStrategy : public LogStrategy{public:// 构造函数,建⽴出来指定的⽬录结构和⽂件结构FileLogStrategy(const std::string logpath = defaultpath, std::string logfilename = defaultname): _logpath(logpath), _logfilename(logfilename){LockGuard lockguard(_mutex);if (std::filesystem::exists(_logpath))return;try{std::filesystem::create_directories(_logpath);}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << '\n';}}// 将⼀条⽇志信息写⼊到⽂件中 void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::string log = _logpath + _logfilename;std::ofstream out(log.c_str(), std::ios::app); // 追加⽅式if (!out.is_open())return;out << message << "\n";out.close();}~FileLogStrategy(){// std::cout << "~FileLogStrategy" << std::endl; // for debug}public:std::string _logpath;std::string _logfilename;Mutex _mutex; // 保证输出线程安全,粗狂⽅式下,可以不⽤};// 具体的⽇志类class Logger{public:Logger(){// 默认使⽤显⽰器策略,如果⽤⼾⼆次指明了策略,会释放在申请,测试的时候注意析构次数UseConsoleStrategy();}~Logger(){}void UseConsoleStrategy(){_strategy = std::make_unique<ConsoleLogStrategy>();}void UseFileStrategy(){_strategy = std::make_unique<FileLogStrategy>();}// 内部类,实现RAII⻛格的⽇志格式化和刷新// 这个LogMessage,表⽰⼀条完整的⽇志对象class LogMessage{private:LogLevel _type;         // ⽇志等级std::string _curr_time; // ⽇志时间pid_t _pid;             // 写⼊⽇志的时间std::string _filename;  // 对应的⽂件名int _line;              // 对应的⽂件⾏号Logger &_logger;        // 引⽤外部logger类, ⽅便使⽤策略进⾏刷新std::string _loginfo;   // ⼀条合并完成的,完整的⽇志信息public:// RAII⻛格,构造的时候构建好⽇志头部信息LogMessage(LogLevel type, std::string &filename, int line, Logger &logger): _type(type),_curr_time(GetCurrTime()),_pid(getpid()),_filename(filename),_line(line),_logger(logger){// stringstream不允许拷⻉,所以这⾥就当做格式化功能使⽤std::stringstream ssbuffer;ssbuffer << "[" << _curr_time << "] "<< "[" << LogLevelToString(type) << "] "<< "[" << _pid << "] "<< "[" << _filename << "] "<< "[" << _line << "]"<< " - ";_loginfo = ssbuffer.str();}// 重载<< ⽀持C++⻛格的⽇志输⼊,使⽤模版,表⽰⽀持任意类型 template <typename T>LogMessage &operator<<(const T &info){std::stringstream ssbuffer;ssbuffer << info;_loginfo += ssbuffer.str();return *this; // 返回当前LogMessage对象,⽅便下次继续进⾏<<}// RAII⻛格,析构的时候进⾏⽇志持久化,采⽤指定的策略~LogMessage(){if (_logger._strategy){_logger._strategy->SyncLog(_loginfo);}// std::cout<< "~LogMessage" << std::endl;}};// 故意拷⻉,形成LogMessage临时对象,后续在被<<时,会被持续引⽤,// 直到完成输⼊,才会⾃动析构临时LogMessage,⾄此也完成了⽇志的显⽰或者刷新// 同时,形成的临时对象内包含独⽴⽇志数据// 未来采⽤宏替换,进⾏⽂件名和代码⾏数的获取LogMessage operator()(LogLevel type, std::string filename, int line){return LogMessage(type, filename, line, *this);}private:std::unique_ptr<LogStrategy> _strategy; // 写⼊⽇志的策略};// 定义全局的logger对象Logger logger;
// 使⽤宏,可以进⾏代码插⼊,⽅便随时获取⽂件名和⾏号
#define LOG(type) logger(type, __FILE__, __LINE__)
// 提供选择使⽤何种⽇志策略的⽅法
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy()
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy()
}

1.2 线程池设计

线程池:

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

线程池的应用场景:

  • 要大量的线程来完成任务,且完成任务的时间比较短。比如WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  • 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  • 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误,

线程池的种类

  • 创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中的任务接口
  • 浮动线程池,其他同上

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

ThreadPool.hpp
#pragma once#include <iostream>
#include <vector>
#include <queue>
#include <memory>
#include <pthread.h>
#include "Log.hpp"    // 引⼊⾃⼰的⽇志
#include "Thread.hpp" // 引⼊⾃⼰的线程
#include "Lock.hpp"   // 引⼊⾃⼰的锁
#include "Cond.hpp"   // 引⼊⾃⼰的条件变量using namespace ThreadModule;
using namespace CondModule;
using namespace LockModule;
using namespace LogModule;
const static int gdefaultthreadnum = 10;// ⽇志
template <typename T>
class ThreadPool
{
private:voidHandlerTask() // 类的成员⽅法,也可以成为另⼀个类的回调⽅法,⽅便我们继续类级别的互相调⽤!{std::string name = GetThreadNameFromNptl();LOG(LogLevel::INFO) << name << " is running...";while (true){// 1. 保证队列安全_mutex.Lock();// 2. 队列中不⼀定有数据while (_task_queue.empty() && _isrunning){_waitnum++;_cond.Wait(_mutex);_waitnum--;}// 2.1 如果线程池已经退出了 &&任务队列是空的 if (_task_queue.empty() && !_isrunning){_mutex.Unlock();break;}// 2.2 如果线程池不退出 &&任务队列不是空的// 2.3 如果线程池已经退出 && 任务队列不是空的 --- 处理完所有的任务,然后在退出// 3. ⼀定有任务, 处理任务T t = _task_queue.front();_task_queue.pop();_mutex.Unlock();LOG(LogLevel::DEBUG) << name << " get a task";// 4. 处理任务,这个任务属于线程独占的任务t();}}public:// 是要有的,必须是私有的ThreadPool(int threadnum = gdefaultthreadnum) : _threadnum(threadnum),_waitnum(0), _isrunning(false){LOG(LogLevel::INFO) << "ThreadPool Construct()";}void InitThreadPool(){// 指向构建出所有的线程,并不启动for (int num = 0; num < _threadnum; num++){_threads.emplace_back(std::bind(&ThreadPool::HandlerTask, this));LOG(LogLevel::INFO) << "init thread " << _threads.back().Name() << " done";}}void Start(){_isrunning = true;for (auto &thread : _threads){thread.Start();LOG(LogLevel::INFO) << "start thread " << thread.Name() << "done";}}void Stop(){_mutex.Lock();_isrunning = false;_cond.NotifyAll();_mutex.Unlock();LOG(LogLevel::DEBUG) << "线程池退出中...";}void Wait(){for (auto &thread : _threads){thread.Join();LOG(LogLevel::INFO) << thread.Name() << " 退出...";}}bool Enqueue(const T &t){bool ret = false;_mutex.Lock();if (_isrunning){_task_queue.push(t);if (_waitnum > 0){_cond.Notify();}LOG(LogLevel::DEBUG)<< "任务⼊队列成功";ret = true;}_mutex.Unlock();return ret;}~ThreadPool(){}private:int _threadnum;std::vector<Thread> _threads; // for fix, int tempstd::queue<T> _task_queue;Mutex _mutex;Cond _cond;int _waitnum;bool _isrunning;
}
g++ main.cc -std=c++17 -lpthread // 需要使⽤C++17
$ ./a.out
[2024-08-04 15:09:29] [INFO] [206342] [ThreadPool.hpp] [62] - ThreadPool
Construct()
[2024-08-04 15:09:29] [INFO] [206342] [ThreadPool.hpp] [70] - init thread
Thread-0 done
[2024-08-04 15:09:29] [INFO] [206342] [ThreadPool.hpp] [70] - init thread
Thread-1 done
this is a task
...
this is a task
[2024-08-04 15:09:39] [DEBUG] [206342] [ThreadPool.hpp] [88] - 线程池退出中...
[2024-08-04 15:09:44] [INFO] [206342] [ThreadPool.hpp] [95] - Thread-0 退出...
[2024-08-04 15:09:44] [INFO] [206342] [ThreadPool.hpp] [95] - Thread-1 退出...
[2024-08-04 15:09:44] [INFO] [206342] [ThreadPool.hpp] [95] - Thread-2 退出...
[2024-08-04 15:09:44] [INFO] [206342] [ThreadPool.hpp] [95] - Thread-3 退出...
[2024-08-04 15:09:44] [INFO] [206342] [ThreadPool.hpp] [95] - Thread-4 退出..

1.3 线程安全的单例模式

1.3.1 单例模式的特点

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

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

1.3.2 饿汉实现⽅式和懒汉实现⽅式
  • 吃完饭,立刻洗碗,这种就是饿汉方式,因为下一顿吃的时候可以立刻拿着碗就能吃饭,
  • 吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗,就是懒汉方式,

懒汉⽅式最核⼼的思想是 "延时加载". 从⽽能够优化服务器的启动速度.

1.3.3 饿汉方式实现单例模式
template <typename T>
class Singleton {static T data;
public:static T* GetInstance() {return &data;}
};

只要通过 Singleton 这个包装类来使⽤ T 对象, 则⼀个进程中只有⼀个 T 对象的实例.

1.3.4 懒汉方式实现单例模式
template <typename T>
class Singleton {static T* inst;
public:static T* GetInstance() {if (inst == NULL) {inst = new T();} return inst;}
}

存在⼀个严重的问题, 线程不安全.

第⼀次调⽤ GetInstance 的时候, 如果两个线程同时调⽤, 可能会创建出两份 T 对象的实例.

1.3.5 懒汉方式实现单例模式(线程安全版本)
// 懒汉模式, 线程安全
template <typename T>
class Singleton {volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.static std::mutex lock;
public:static T* GetInstance() {if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提⾼性能.lock.lock(); // 使⽤互斥锁, 保证多线程情况下也只调⽤⼀次 new.if (inst == NULL) {inst = new T();} lock.unlock();} return inst;}
};

注意事项:

  1. 加锁解锁的位置
  2. 双重if判定,避免不必要的锁竞争
  3. volatile关键字防止过度优化

1.4 单例式线程池

ThreadPool.hpp#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include <memory>
#include <pthread.h>#include "Log.hpp" // 引⼊⾃⼰的⽇志
#include "Thread.hpp" // 引⼊⾃⼰的线程
#include "Lock.hpp" // 引⼊⾃⼰的锁
#include "Cond.hpp" // 引⼊⾃⼰的条件变量using namespace ThreadModule;
using namespace CondModule;
using namespace LockModule;
using namespace LogModule;const static int gdefaultthreadnum = 10;// ⽇志
template <typename T>
class ThreadPool
{ 
private:// 是要有的,必须是私有的ThreadPool(int threadnum = gdefaultthreadnum) : _threadnum(threadnum),_waitnum(0),
_isrunning(false){LOG(LogLevel::INFO) << "ThreadPool Construct()";}void InitThreadPool(){// 指向构建出所有的线程,并不启动for (int num = 0; num < _threadnum; num++){_threads.emplace_back(std::bind(&ThreadPool::HandlerTask, this));LOG(LogLevel::INFO) << "init thread " << _threads.back().Name() <<" done";}}void Start(){_isrunning = true;for (auto &thread : _threads){thread.Start();LOG(LogLevel::INFO) << "start thread " << thread.Name() << "done";}}void HandlerTask() // 类的成员⽅法,也可以成为另⼀个类的回调⽅法,⽅便我们继续类级别的互相调⽤!{std::string name = GetThreadNameFromNptl();LOG(LogLevel::INFO) << name << " is running...";while (true){// 1. 保证队列安全_mutex.Lock();// 2. 队列中不⼀定有数据while (_task_queue.empty() && _isrunning){_waitnum++;_cond.Wait(_mutex);_waitnum--;} // 2.1 如果线程池已经退出了 && 任务队列是空的if (_task_queue.empty() && !_isrunning){_mutex.Unlock();break;} 
// 2.2 如果线程池不退出 && 任务队列不是空的
// 2.3 如果线程池已经退出 && 任务队列不是空的 --- 处理完所有的任务,然后在退出
// 3. ⼀定有任务, 处理任务T t = _task_queue.front();_task_queue.pop();_mutex.Unlock();LOG(LogLevel::DEBUG) << name << " get a task";
// 4. 处理任务,这个任务属于线程独占的任务t();}}// 复制拷⻉禁⽤ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;ThreadPool(const ThreadPool<T> &) = delete;
public:static ThreadPool<T> *GetInstance(){// 如果是多线程获取线程池对象下⾯的代码就有问题了!!// 只有第⼀次会创建对象,后续都是获取// 双判断的⽅式,可以有效减少获取单例的加锁成本,⽽且保证线程安全if (nullptr == _instance) // 保证第⼆次之后,所有线程,不⽤在加锁,直接返回_instance单例对象{LockGuard lockguard(_lock);if (nullptr == _instance){_instance = new ThreadPool<T>();_instance->InitThreadPool();_instance->Start();LOG(LogLevel::DEBUG) << "创建线程池单例";return _instance;}} LOG(LogLevel::DEBUG) << "获取线程池单例";return _instance;
}void Stop(){_mutex.Lock();_isrunning = false;_cond.NotifyAll();_mutex.Unlock();LOG(LogLevel::DEBUG) << "线程池退出中...";}void Wait(){for (auto &thread : _threads){thread.Join();LOG(LogLevel::INFO) << thread.Name() << " 退出...";}}bool Enqueue(const T &t){bool ret = false;_mutex.Lock();if (_isrunning){_task_queue.push(t);if (_waitnum > 0){_cond.Notify();} LOG(LogLevel::DEBUG) << "任务⼊队列成功";ret = true;}_mutex.Unlock();return ret;} ~ThreadPool(){}private:int _threadnum;std::vector<Thread> _threads; // for fix, int tempstd::queue<T> _task_queue;Mutex _mutex;Cond _cond;int _waitnum;bool _isrunning;// 添加单例模式static ThreadPool<T> *_instance;static Mutex _lock;
};template <typename T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;template <typename T>
Mutex ThreadPool<T>::_lock;

测试样例代码

#include <iostream>
#include <functional>
#include <unistd.h>
#include "ThreadPool.hpp"using task_t = std::function<void()>;void DownLoad()
{std::cout << "this is a task" << std::endl;
}int main()
{ENABLE_CONSOLE_LOG_STRATEGY();int cnt = 10;while(cnt){ThreadPool<task_t>::GetInstance()->Enqueue(DownLoad);sleep(1);cnt--;} ThreadPool<task_t>::GetInstance()->Stop();sleep(5);ThreadPool<task_t>::GetInstance()->Wait();return 0;
}
$ ./a.out
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [28] - ThreadPool
Construct()
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread
Thread-0 done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread
Thread-1 done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread
Thread-2 done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread
Thread-3 done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread
Thread-4 done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread
Thread-5 done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread
Thread-6 done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread
Thread-7 done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread
Thread-8 done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread
Thread-9 done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread
Thread-0done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread
Thread-1done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [51] - Thread-0 is
running...
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread
Thread-2done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread
Thread-3done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [51] - Thread-2 is
running...
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread
Thread-4done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [51] - Thread-3 is
running...
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread
Thread-5done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [51] - Thread-4 is
running...
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [51] - Thread-5 is
running...
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread
Thread-6done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [51] - Thread-6 is
running...
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread
Thread-7done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [51] - Thread-7 is
running...
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread
Thread-8done
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread
Thread-9done
[2024-08-04 15:03:37] [DEBUG] [206234] [ThreadPool.hpp] [98] - 创建线程池单例
[2024-08-04 15:03:37] [DEBUG] [206234] [ThreadPool.hpp] [133] - 任务⼊队列成功
[2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [51] - Thread-1 is
running...
[2024-08-04 15:03:37] [DEBUG] [206234] [ThreadPool.hpp] [75] - Thread-0 get a
task
this is a task
....
[2024-08-04 15:03:47] [DEBUG] [206234] [ThreadPool.hpp] [102] - 获取线程池单例
[2024-08-04 15:03:47] [DEBUG] [206234] [ThreadPool.hpp] [112] - 线程池退出中...
[2024-08-04 15:03:52] [DEBUG] [206234] [ThreadPool.hpp] [102] - 获取线程池单例
[2024-08-04 15:03:52] [INFO] [206234] [ThreadPool.hpp] [119] - Thread-0 退出...
[2024-08-04 15:03:52] [INFO] [206234] [ThreadPool.hpp] [119] - Thread-1 退出...
[2024-08-04 15:03:52] [INFO] [206234] [ThreadPool.hpp] [119] - Thread-2 退出...
[2024-08-04 15:03:52] [INFO] [206234] [ThreadPool.hpp] [119] - Thread-3 退出...
[2024-08-04 15:03:52] [INFO] [206234] [ThreadPool.hpp] [119] - Thread-4 退出...
[2024-08-04 15:03:52] [INFO] [206234] [ThreadPool.hpp] [119] - Thread-5 退出...
http://www.dtcms.com/a/355461.html

相关文章:

  • 【mysql】SQL HAVING子句详解:分组过滤的正确姿势
  • SystemVerilog学习【六】功能覆盖率详解
  • OpenCV 4.9+ 进阶技巧与优化
  • Shell编程(一)
  • 流线型(2型)通风排烟天窗/TPC-A2
  • LoRA modules_to_save解析及卸载适配器(62)
  • C语言学习-24-柔性数组
  • 科技守护古树魂:古树制茶行业的数字化转型之路
  • TikTok 在电脑也能养号?网页端多号养号教程
  • 损失函数,及其优化方法
  • [Ai Agent] 从零开始搭建第一个智能体
  • 麒麟操作系统挂载NAS服务器
  • 搜维尔科技核心产品矩阵涵盖从硬件感知到软件渲染的全产品供应链
  • 12KM无人机高清图传通信模组——打造未来空中通信新高度
  • hintcon2025 Pholyglot!
  • 辅助驾驶出海、具身智能落地,稀缺的3D数据从哪里来?
  • kubernetes-ubuntu24.04操作系统部署k8s集群
  • 吃透 OpenHarmony 资源调度:核心机制、调度策略与多设备协同实战
  • Linux(二) | 文件基本属性与链接扩展
  • ManusAI:多语言手写识别的技术革命
  • SLF4J和LogBack
  • Linux 命令使用案例:文件和目录管理
  • 从0开始学习Java+AI知识点总结-27.web实战(Maven高级)
  • Python Imaging Library (PIL) 全面指南:PIL基础入门-图像滤波与处理技术
  • python自动化测试工具selenium使用指南
  • AS32S601抗辐照MCU在商业卫星EDFA系统中的应用研究
  • 基于 Selenium 和 BeautifulSoup 的动态网页爬虫:一次对百度地图 POI 数据的深度模块化剖析
  • 033 日志
  • 硬件三人行--运算基础篇
  • 怎样将Word转成高质量的DITA