1.概念
基本概念
线程池是一种多线程处理形式,它预先创建一组线程并管理它们,避免频繁创建和销毁线程带来的性能开销。
在 Linux 环境下,线程池(Thread Pool)是一种常用的并发编程模型,用于复用线程资源 ,避免频繁创建和销毁线程带来的性能开销。它特别适用于高并发、短任务的场景,比如 Web 服务器、数据库连接池、异步任务处理等
为什么需要线程池
- 降低资源消耗:重复利用已创建的线程
- 提高响应速度:任务到达时可直接执行,无需等待线程创建
- 提高线程可管理性:统一分配、调优和监控
- 防止资源耗尽:避免无限制创建线程导致系统崩溃
✅ 线程池的核心组件
| 组件 | 作用说明 |
|---|---|
| 任务队列 | 存放待执行的任务(函数指针、lambda、function 等) |
| 工作线程 | 从任务队列中取出任务并执行 |
| 同步机制 | 使用互斥锁和条件变量实现线程间通信 |
| 线程复用 | 线程执行完任务后不退出,而是继续等待下一个任务 |
✅ 线程池的优势
| 优势 | 说明 |
|---|---|
| 性能提升 | 避免频繁创建/销毁线程 |
| 资源控制 | 限制最大并发线程数,防止系统过载 |
| 任务调度 | 可配合优先级队列、延迟任务等机制 |
✅ 线程池的注意事项
| 问题 | 解决方案 |
|---|---|
| 任务阻塞 | 避免在线程池中执行阻塞 IO,必要时使用异步 IO |
| 异常处理 | 任务中抛出的异常不会自动传播,需手动捕获 |
线程池作用
线程池是一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
线程池的应用场景
• 需要大量的线程来完成任务,且完成任务的时间比较短。 比如WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
• 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
• 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.
线程池的种类
a. 创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中的任务接口
b. 浮动线程池,其他同上
此处,我们选择固定线程个数的线程池。

2.实现
前置头文件
cpp
thread_pool_test:thread_pool_test.pp
g++ -o $@ $^ -std=c++17 -lpthread
.PHONY:clean
clean:
rm -f thread_pool_test
cpp
#pragma once
#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"
class Cond
{
public:
Cond()
{
pthread_cond_init(&_cond, nullptr);
}
void Wait(Mutex &lock)
{
int n = pthread_cond_wait(&_cond, lock.Get());
}
void NotifyOne()
{
int n = pthread_cond_signal(&_cond);
(void)n;
}
void NotifyAll()
{
int n = pthread_cond_broadcast(&_cond);
(void)n;
}
~Cond()
{
pthread_cond_destroy(&_cond);
}
private:
pthread_cond_t _cond;
};
cpp
#pragma once
#include <iostream>
#include <mutex>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
pthread_mutex_t *Get()
{
return &_lock;
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
class LockGuard
{
public:
LockGuard(Mutex *_mutex):_mutexp(_mutex)
{
_mutexp->Lock();
}
~LockGuard()
{
_mutexp->Unlock();
}
private:
Mutex *_mutexp;
};
cpp
#pragma once
#include <iostream>
#include <string>
#include <filesystem> // C++17 文件操作
#include <fstream>
#include <ctime>
#include <unistd.h>
#include <memory>
#include <sstream>
#include "Mutex.hpp"
// 规定出场景的日志等级
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
// 日志转换成为字符串
std::string Level2String(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";
}
}
// 根据时间戳,获取可读性较强的时间信息
// 20XX-08-04 12:27:03
std::string GetCurrentTime()
{
// 1. 获取时间戳
time_t currtime = time(nullptr);
// 2. 如何把时间戳转换成为20XX-08-04 12:27:03
struct tm currtm;
localtime_r(&currtime, &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;
}
// 策略模式,策略接口
// 1. 刷新的问题 -- 假设我们已经有了一条完整的日志,string->设备(显示器,文件)
// 基类方法
class LogStrategy
{
public:
// 不同模式核心是刷新方式的不同
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &logmessage) = 0;
};
// 控制台日志策略,就是日志只向显示器打印,方便我们debug
// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:
~ConsoleLogStrategy()
{
}
void SyncLog(const std::string &logmessage) override
{
{
LockGuard lockguard(&_lock);
std::cout << logmessage << std::endl;
}
}
private:
// 显示器也是临界资源,保证输出线程安全
Mutex _lock;
};
// 默认路径和日志名称
const std::string logdefaultdir = "log";
const static std::string logfilename = "test.log";
// 文件日志策略
// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:
// 构造函数,建立出来指定的目录结构和文件结构
FileLogStrategy(const std::string &dir = logdefaultdir,
const std::string filename = logfilename)
: _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() << "\r\n";
}
}
// 将一条日志信息写入到文件中
void SyncLog(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); // append
if (!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/hello.log
Mutex _lock;
};
// 具体的日志类
// 1. 定制刷新策略
// 2. 构建完整的日志
class Logger
{
public:
Logger()
{
}
void EnableConsoleLogStrategy()
{
_strategy = std::make_unique<ConsoleLogStrategy>();
}
void EnableFileLogStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
// 内部类,实现RAII风格的日志格式化和刷新
// 这个LogMessage,表示一条完整的日志对象
class LogMessage
{
public:
// RAII风格,构造的时候构建好日志头部信息
LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
: _curr_time(GetCurrentTime()),
_level(level),
_pid(getpid()),
_filename(filename),
_line(line),
_logger(logger)
{
// stringstream不允许拷贝,所以这里就当做格式化功能使用
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << Level2String(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "]"
<< " - ";
_loginfo = ss.str();
}
// 重载 << 支持C++风格的日志输入,使用模版,表示支持任意类型
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
// RAII风格,析构的时候进行日志持久化,采用指定的策略
~LogMessage()
{
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临时对象,后续在被<<时,会被持续引用,
// 直到完成输入,才会自动析构临时LogMessage,至此也完成了日志的显示或者刷新
// 同时,形成的临时对象内包含独立日志数据
// 未来采用宏替换,进行文件名和代码行数的获取
LogMessage operator()(LogLevel level, std::string filename, int line)
{
return LogMessage(level, filename, line, *this);
}
~Logger()
{
}
private:
// 写入日志的策略
std::unique_ptr<LogStrategy> _strategy;
};
// 定义全局的logger对象
Logger logger;
// 使用宏,可以进行代码插入,方便随时获取文件名和行号
#define LOG(level) logger(level, __FILE__, __LINE__)
// 提供选择使用何种日志策略的方法
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() logger.EnableFileLogStrategy()
cpp
#pragma once
#include <iostream>
#include <functional>
#include <unistd.h>
#include <sstream>
class Task
{
public:
Task()
{}
Task(int x, int y): a(x), b(y)
{
}
void Execute()
{
result = a + b;
}
void operator()()
{
// sleep(1);
Execute();
}
void Print()
{
std::cout << a << " + " << b << " = " << result << std::endl;
}
std::string Result2String()
{
std::stringstream ss;
ss << a << " + " << b << " = " << result;
return ss.str();
}
private:
int a;
int b;
int result;
};
cpp
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <functional>
#include <sys/syscall.h> /* For SYS_xxx definitions */
#include "Logger.hpp"
#define get_lwp_id() syscall(SYS_gettid)
using func_t = std::function<void(const std::string&name)>;
const std::string threadnamedefault = "None-Name";
class Thread
{
public:
Thread(func_t func, const std::string &name = threadnamedefault)
: _name(name),
_func(func),
_isrunning(false)
{
LOG(LogLevel::INFO) << _name << " create thread obj success";
}
static void *start_routine(void *args)
{
Thread *self = static_cast<Thread *>(args);
self->_isrunning = true;
self->_lwpid = get_lwp_id();
self->_func(self->_name);
pthread_exit((void *)0);
}
void Start()
{
int n = pthread_create(&_tid, nullptr, start_routine, this);
if (n == 0)
{
LOG(LogLevel::INFO) << _name << " running success";
}
}
void Stop()
{
int n = pthread_cancel(_tid); // 太简单粗暴了
(void)n;
}
// void Die()
// {
// pthread_cancel(_tid);
// }
// 检测线程结束并且回收的功能
void Join()
{
if (!_isrunning)
return;
int n = pthread_join(_tid, nullptr);
if (n == 0)
{
LOG(LogLevel::INFO) << _name << " pthread_join success";
}
}
~Thread()
{
// LOG(LogLevel::INFO) << _name << " destory thread obj success";
}
private:
bool _isrunning;
pthread_t _tid;
pid_t _lwpid;
std::string _name;
func_t _func;
};
thread_pool
cpp
#pragma once
#include <queue>
#include <vector>
#include "Thread.hpp"
#include "Cond.hpp"
#include "Logger.hpp"
using namespace std;
const static int default_thread_num = 5;
template <typename T>
class ThreadPool
{
void Routine(string name)
{
while (true)
{
T t;
{
LockGuard lock(&_mutex);
// 如果线程池正在运行且任务队列为空
// 注意,一定要使用while,防止出错
while (_is_running && QueueIsEmpty())
{
_wait_thread_num++;
_cond.Wait(_mutex); // 线程唤醒后执行的是这里的逻辑 如果队列为空了 就要离开循环了
_wait_thread_num--;
}
// 如果线程池要退出且任务队列为空
if (!_is_running && QueueIsEmpty())
{
LOG(LogLevel::INFO) << "线程池准备退出&&任务队列为空 " << name.c_str() << "退出";
break;
}
// 此时任务队列一定不为空,存在两种情况
// 1. 线程池准备退出 -- 消耗历史任务
// 2. 线程池没有准备退出 -- 正常工作
t = _task_queue.front();
_task_queue.pop();
// LOG(LogLevel::DEBUG) << name << "::::" <<_task_queue.size();
// if (!QueueIsEmpty())
// {
// t = _task_queue.front();
// //Linux上面实现的stl库,队列为空的时候(size==0)还可以去数据导致(size--)
// //拿到了一个0+0 = 0的任务
// //然后size(size_t类型的数据没有负数)就变成了一个非常大的正数,队列就不为空了
// //这里段错误和数组的越界访问类似 然后一直拿去数据 触发了段错误
// _task_queue.pop();
// }
}
// 此时,线程已经把任务从临界资源获取到线程私有!临界区 -> 线程私有的栈
// 处理任务时不需要再临界区内部进行,并发进行效率更高
t(); // 规定,未来的任务必须这样处理!operate()重载
LOG(LogLevel::DEBUG) << name << " handler task: " << t.Result2String();
}
}
public:
ThreadPool(int threadnum = default_thread_num)
: _thread_num(threadnum),
_wait_thread_num(0),
_is_running(false)
{
for (int i = 1; i <= _thread_num; i++)
{
// 方法1:
// auto f = std::bind(hello, this);
// 方法2
string name = "thread-" + to_string(i);
// emplace_back()是STL容器(如vector、deque、list)的成员函数,
// 用于在容器尾部直接构造元素,避免不必要的拷贝或移动。
_threads.emplace_back([this](const string &name)
{ this->Routine(name); }, name);
}
LOG(LogLevel::INFO) << "thread pool obj create success";
}
void Start()
{
if (_is_running)
return;
_is_running = true;
for (auto &t : _threads)
t.Start();
}
void Stop()
{
// 线程池要退出,不是立刻就能退出的,它要把任务队列中的任务处理完后才能退出
if (!_is_running)
return;
_is_running = false;
// 线程池都要退出了,那些休眠的线程还休眠什么,赶紧把他们全部唤醒
// 处理完任务后线程池好退出
if (_wait_thread_num)
_cond.NotifyAll();
}
void Enqueue(const T &task)
{
// 如果线程池准备退出,任务就不要入队列了
if (!_is_running)
return;
{
LockGuard lock(&_mutex);
_task_queue.push(task);
// LOG(LogLevel::DEBUG) << "一个任务入队列了";
if (_wait_thread_num)
_cond.NotifyOne();
}
}
void Wait()
{
for (auto &t : _threads)
{
t.Join();
}
LOG(LogLevel::INFO) << "thread pool wait success";
}
bool QueueIsEmpty()
{
return _task_queue.empty();
}
~ThreadPool()
{
}
private:
// 任务队列
queue<T> _task_queue; // 整体使用的临界资源
vector<Thread> _threads;
int _thread_num; // 线程池中线程个数
int _wait_thread_num; // 线程池正在等待的线程个数
// 保护线程池安全
Mutex _mutex;
Cond _cond;
// 检测线程池是否在运行
bool _is_running;
};
cpp
#include"ThreadPool.hpp"
#include "Task.hpp"
#include<time.h>
#include<memory>
int main()
{
srand(time(nullptr));
//使用控制台策略,向显示器输出日志
EnableConsoleLogStrategy();
unique_ptr<ThreadPool<Task>> tp=make_unique<ThreadPool<Task>>();
tp->Start();
int cnt=10;
while(cnt--)
{
int x=rand()%9+1;
int y=rand()%9+1;
Task task(x,y);
tp->Enqueue(task);
sleep(1);
}
tp->Stop();
tp->Wait();
return 0;
}

注意若是if{...}在while{...}前面就会出错

如下:

3 线程池单例模式
3.1 概念
单例模式确保一个类只有一个实例,并提供一个全局访问点。
线程池使用单例模式的理由:
- 系统中通常只需要一个全局的线程池
- 避免资源浪费(多个线程池竞争系统资源)
- 便于统一管理和监控
3.2 饿汉实现方式和懒汉实现方式
洗碗的例子
吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.
懒汉方式最核心的思想是 "延时加载". 从而能够优化服务器的启动速度
核心区别:
| 特性 | 饿汉式 (Eager) | 懒汉式 (Lazy) |
|---|---|---|
| 初始化时机 | 程序启动时/类加载时 | 第一次使用时 |
| 线程安全 | 天然线程安全 | 需要额外同步机制 |
| 资源占用 | 启动时即占用资源 | 使用时才占用资源 |
| 性能 | 启动慢,运行时快 | 启动快,第一次使用稍慢 |
| 实现复杂度 | 简单 | 较复杂 |
3.2.1 饿汉实现方式
核心思想
"提前准备,立即加载" - 在程序启动或类加载时就创建实例。
cpp
template <typename T>
class Singleton {
static T data;//静态全局变量定义在全局数据区,编译后就会为它分配空间
public:
static T* GetInstance()
{
return &data;
}
};
只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例.
饿汉式特点分析
优点:
- 线程安全:无需考虑多线程同步问题
- 性能好:获取实例时直接返回,无锁开销
- 实现简单:代码简洁,不易出错
缺点:
- 启动时间:可能增加程序启动时间
- 资源浪费:即使不使用也占用资源
- 初始化顺序:不同编译单元间的静态变量初始化顺序不确定
适用场景:
- 单例初始化开销小
- 单例在程序运行中必定会被使用
- 对性能要求极高,不能忍受锁开销
- 多线程环境且不想处理同步问题
3.2.2 懒汉实现方式
核心思想
"用时创建,延迟加载" - 在第一次请求时才创建实例。
cpp
template <typename T>
class Singleton {
static T* inst;
public:
static T* GetInstance()
{
if (inst == NULL) {
inst = new T();
}
return inst;
}
};
存在一个严重的问题, 线程不安全.
第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例.
但是后续再次调用, 就没有问题了
懒汉式特点分析
优点:
- 延迟加载:只有使用时才创建,节省资源
- 启动快速:不增加程序启动时间
- 灵活性高:可以根据需要动态创建
缺点:
- 线程安全复杂:需要处理多线程同步
- 首次访问慢:第一次获取实例需要创建和同步
- 实现复杂:需要考虑各种边界情况
适用场景:
- 单例初始化开销大
- 单例可能不会被使用
- 系统资源有限,需要按需加载
- 对启动时间敏感的应用
3.2.3 深度对比与分析
内存模型分析
cpp
// 饿汉式内存布局
// 编译时确定,在数据段分配内存
// 程序加载时即初始化
// 懒汉式内存布局
// 运行时确定,在堆上分配内存
// 第一次访问时初始化
线程安全性对比
| 实现方式 | 饿汉式 | 懒汉式 |
|---|---|---|
| 基础实现 | 线程安全 | 线程不安全 |
| 加锁实现 | 不需要 | 需要锁机制 |
| 双重检查 | 不需要 | 需要DCLP |
| 现代实现 | 总是安全 | call_once或局部静态变量 |
选择指南
选择饿汉式的情况:
- 单例的创建和运行时开销非常小
- 程序运行过程中一定会使用该单例
- 希望避免任何锁开销
- 单例的初始化不依赖其他未初始化的资源
选择懒汉式的情况:
- 单例初始化成本高(连接数据库、读取大文件等)
- 单例可能在整个程序生命周期中都不被使用
- 单例的初始化依赖运行时信息或其他单例
- 需要动态控制单例的创建时机
3.2.4 总结
| 特性 | 饿汉式 | 懒汉式 |
|---|---|---|
| 哲学 | 积极准备 | 按需分配 |
| 实现 | 简单直接 | 复杂但灵活 |
| 线程安全 | 天然安全 | 需额外保障 |
| 资源使用 | 启动时占用 | 使用时占用 |
| 性能特点 | 启动慢使用快 | 启动快第一次慢 |
| 现代推荐 | 静态成员或局部静态变量 | 局部静态变量(call_once) |
最佳实践建议:
- 优先使用基于局部静态变量的懒汉式(C++11及以上)
- 如果确定单例一定被使用且初始化快,可以考虑饿汉式
- 在多线程环境中,避免手写双重检查锁定,使用标准库提供的线程安全机制
- 考虑是否真的需要单例,评估是否有更好的设计模式替代
3.3 懒汉方式实现单例模式
其他的头文件都和上面一样
cpp
ThreadPool:ThreadPool.cpp
g++ -o $@ $^ -std=c++17 -lpthread
.PHONY:clean
clean:
rm -f ThreadPool
cpp
#pragma once
#include <queue>
#include <vector>
#include "Thread.hpp"
#include "Cond.hpp"
#include "Logger.hpp"
using namespace std;
// 单例线程池 - 懒汉模式
const static int default_thread_num = 3;
template <typename T>
class ThreadPool
{
void Routine(string name)
{
while (true)
{
T t;
{
LockGuard lock(&_mutex);
// 如果线程池正在运行且任务队列为空
// 注意,一定要使用while,防止出错
while (_is_running && QueueIsEmpty())
{
_wait_thread_num++;
_cond.Wait(_mutex); // 线程唤醒后执行的是这里的逻辑 如果队列为空了 就要离开循环了
_wait_thread_num--;
}
// 如果线程池要退出且任务队列为空
if (!_is_running && QueueIsEmpty())
{
LOG(LogLevel::INFO) << "线程池准备退出&&任务队列为空 " << name.c_str() << "退出";
break;
}
// 此时任务队列一定不为空,存在两种情况
// 1. 线程池准备退出 -- 消耗历史任务
// 2. 线程池没有准备退出 -- 正常工作
t = _task_queue.front();
_task_queue.pop();
// LOG(LogLevel::DEBUG) << name << "::::" <<_task_queue.size();
// if (!QueueIsEmpty())
// {
// t = _task_queue.front();
// //Linux上面实现的stl库,队列为空的时候(size==0)还可以去数据导致(size--)
// //拿到了一个0+0 = 0的任务
// //然后size(size_t类型的数据没有负数)就变成了一个非常大的正数,队列就不为空了
// //这里段错误和数组的越界访问类似 然后一直拿去数据 触发了段错误
// _task_queue.pop();
// }
}
// 此时,线程已经把任务从临界资源获取到线程私有!临界区 -> 线程私有的栈
// 处理任务时不需要再临界区内部进行,并发进行效率更高
t(); // 规定,未来的任务必须这样处理!operate()重载
LOG(LogLevel::DEBUG) << name << " handler task: " << t.Result2String();
}
}
private:
// 将构造函数设为私有,不允许用户直接创建对象
ThreadPool(int threadnum = default_thread_num)
: _thread_num(threadnum),
_wait_thread_num(0),
_is_running(false)
{
for (int i = 1; i <= _thread_num; i++)
{
// 方法1:
// auto f = std::bind(hello, this);
// 方法2
string name = "thread-" + to_string(i);
// emplace_back()是STL容器(如vector、deque、list)的成员函数,
// 用于在容器尾部直接构造元素,避免不必要的拷贝或移动。
_threads.emplace_back([this](const string &name)
{ this->Routine(name); }, name);
}
LOG(LogLevel::INFO) << "thread pool obj create success";
}
// 禁掉拷贝构造和赋值重载
ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
ThreadPool(const ThreadPool<T> &) = delete;
public:
void Start()
{
if (_is_running)
return;
_is_running = true;
for (auto &t : _threads)
t.Start();
}
void Stop()
{
// 线程池要退出,不是立刻就能退出的,它要把任务队列中的任务处理完后才能退出
if (!_is_running)
return;
_is_running = false;
// 线程池都要退出了,那些休眠的线程还休眠什么,赶紧把他们全部唤醒
// 处理完任务后线程池好退出
if (_wait_thread_num)
_cond.NotifyAll();
}
void Enqueue(const T &task)
{
// 如果线程池准备退出,任务就不要入队列了
if (!_is_running)
return;
{
LockGuard lock(&_mutex);
_task_queue.push(task);
// LOG(LogLevel::DEBUG) << "一个任务入队列了";
if (_wait_thread_num)
_cond.NotifyOne();
}
}
void Wait()
{
for (auto &t : _threads)
{
t.Join();
}
LOG(LogLevel::INFO) << "thread pool wait success";
}
bool QueueIsEmpty()
{
return _task_queue.empty();
}
// 用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针
// 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针
// 静态成员函数只能访问静态成员,非静态的成员函数,可以访问任意的静态成员变量和静态成员函数
// 地址转字符串,证明多线程申请的单例都是同一个
static std::string ToHex(ThreadPool<T> *addr)
{
char buffer[64];
snprintf(buffer, sizeof(buffer), "%p", addr);
return buffer;
}
// 获取单例
static ThreadPool<T> *GetInstance()
{
// A, B, c
// 线程安全,提高效率式的获取单例
// 双重 if 判定, 避免不必要的锁竞争
if (!_instance)
{
// 外层if是为了防止获取单例后多线程申请单例时判断前还要申请锁,降低效率
// 保证第二次之后,所有线程,不用在加锁,直接返回_instance单例对象
LockGuard lockguard(&_singleton_lock);
if (!_instance)
{
_instance = new ThreadPool<T>();
LOG(LogLevel::DEBUG) << "线程池单例首次被使用,创建并初始化, addr: " << ToHex(_instance);
_instance->Start();
}
}
else
{
LOG(LogLevel::DEBUG) << "线程池单例已经存在,直接获取, addr: " << ToHex(_instance);
}
return _instance;
}
~ThreadPool()
{
}
private:
// 任务队列
queue<T> _task_queue; // 整体使用的临界资源
vector<Thread> _threads;
int _thread_num; // 线程池中线程个数
int _wait_thread_num; // 线程池正在等待的线程个数
// 保护线程池安全
Mutex _mutex;
Cond _cond;
// 检测线程池是否在运行
bool _is_running;
// 单例中静态指针
// 需要设置 volatile 关键字, 防止被编译器优化.
// volatile static ThreadPool<T> *_instance;
// 用static修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化。
// 类内定义,类外初始化
// 添加单例模式
static ThreadPool<T> *_instance;
static Mutex _singleton_lock;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;
template <class T>
Mutex ThreadPool<T>::_singleton_lock;
cpp
#include "Task.hpp"
#include "ThreadPool.hpp"
#include <memory>
#include <time.h>
int main()
{
srand(time(nullptr) ^ getpid());
EnableConsoleLogStrategy();
//编译时会报错,单例模式不允许用户直接创建线程池对象
// std::unique_ptr<ThreadPool<Task>> tp = std::make_unique<ThreadPool<Task>>(10);
// tp->Start();
int cnt = 10;
while (cnt--)
{
// 生产任务
int x = rand() % 10 + 1;
usleep(rand() % 73);
int y = rand() % 5 + 1;
Task t(x, y);
// push到线程池中,处理
// 突破类域就可以访问静态成员,可以通过 类名::静态成员(不需要对象就能调用)
// 或者 对象.静态成员 来访问静态成员变量和静态成员函数
ThreadPool<Task>::GetInstance()->Enqueue(t);
sleep(1);
}
ThreadPool<Task>::GetInstance()->Stop();
ThreadPool<Task>::GetInstance()->Wait();
return 0;
}