目录
一、简介
线程池(Thread Pool)是一种多线程处理技术,它预先创建并维护一组线程(线程集合),用于处理任务。线程池技术通过重用现有的线程而不是为每个新任务都创建新线程,从而显著提高了应用程序的性能和响应速度,因此线程池在并发环境下发挥着重要作用。详细点来说,线程池具有以下几个优势:
1.线程重用:线程池中的线程在完成任务后不会立即销毁,而是等待新的任务,从而实现线程的重用。
2.控制并发数:通过限制线程池中的线程数量,可以控制并发的任务数量,防止系统资源(如CPU和内存)的过度使用。
3.提高响应速度:当任务到达时,如果线程池中有空闲线程,任务可以立即被执行,而无需等待新线程的创建。
4.管理方便:线程池提供了统一的线程管理接口,简化了多线程编程的复杂性。
总而言之,通过线程池技术能够更高效的使用线程,方便统一对线程进行调度和管理,以及可以防止线程的过度开辟等等。
二、环境准备
以下是笔者的开发环境:
操作系统:win10;
构建工具:cmake 3.28;
编译器:g++ 12.2.0;
c++:c++20
ps: 如果使用visual studio配合msvc也没有问题,只需要确保自己使用的是c++20标准。
三、代码实现
1.创建文件
这里我们需要创建两个文件,一个thread_pool.hpp,另一个是main.cpp,分别用来构建线程池的主体代码和用于测试。
目录结构如下:
然后cmake脚本文件如下(使用visual studio可以忽略此步)
cmake_minimum_required(VERSION 3.28)
project(templateTest)
set(CMAKE_CXX_STANDARD 20)
add_executable(threadPool ThreadPool/main.cpp
ThreadPool/thread_pool.hpp)
然后在thread_pool.hpp中,我们先把之后需要用到的头文件都全部先导进来。
// thread_pool.hpp
#include <iostream>
#include <thread>
#include <utility>
#include <vector>
#include <functional>
#include <queue>
#include <cassert>
#include <mutex>
#include <condition_variable>
2. 编写任务类Task结构体
Task结构体既是对任务的封装,外界想要并发执行的任务在线程池中都将以Task的形式提交并执行。
Task结构体的成员变量很简单,一个整型
_priority
用于指定该任务的执行优先级,值越大优先级越高,越可能被优先执行。还需要一个std::function<void()>
类型的变量_execute_task
来存储实际要执行的任务逻辑,然后再简单书写一下构造函数,Task类就编写完成。
// thread_pool.hpp
struct Task {
//任务优先级
int _priority{0};
//lambda,线程需要执行的具体任务逻辑
std::function<void()> _execute_task;
//构造函数
Task() = default;
Task(std::function<void()> &&task) : _execute_task{task} {}
Task(std::function<void()> &&task, int priority) : _execute_task{task}, _priority{priority} {}
};
3. 编写Job类
结构体Job
是对Task
的进一步封装,它的主要作用是在Task
的基础上提供了优先级排序定义的功能。对任务排序的优先级规则如下:优先以指定的优先级大小(_priority
)进行排序,优先级高的任务排在前,如果两个任务的优先级相同,则以任务的创建时间先后来排序,最先创建的任务排在最前。
因此Job结构体只需要包含两个成员变量,一个是
Task
类本身,一个是time_t
类型的_creat_time
,用来存储任务的创建时间。最后,**我们需要重载<
运算符,来让之后在线程池中存储任务的优先队列能够对任务进行排序。**Job结构体的代码如下:
// thread_pool.hpp
struct Job {
//封装Task
Task _task{};
//任务的创建时间
time_t _creat_time{};
//重载<运算符,方便之后优先队列的排序
bool operator<(const Job &other) const;
//构造函数
Job(Task task) : _task{std::move(task)} {
//初始化任务创建的时间
this->_creat_time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
}
};
//重载<运算符的具体逻辑,优先以优先级排序,优先级相同以创建的时间先后排序
bool Job::operator<(const Job &other) const {
return this->_task._priority == other._task._priority ? this->_creat_time < other._creat_time :
this->_task._priority < other._task._priority;
}
4.线程池类代码的编写
(1) 私有成员变量
在线程池类中我们会有两个十分重要成员变量,一个是priority_queue
类型的队列_task_queue
,另一个则是vector
类型的数组_thread_list
_task_queue
: 使用priority_queue
类型的队列来存储需要线程池处理的任务,该类型的队列底层采用大顶堆的数据结构,会对插入的数据按优先级进行排序,使用该队列,我们可以在想队列中添加任务是,让优先级高的任务更加靠近队头(更先出队)
_thread_list
:使用vector
的数据类型来存储预先开辟好的线程。无论任务队列中有多少个任务,最终都只会被线程数组中的线程执行,而不会额外开辟线程。
然后我们还需要一些用于保证线程安全以及确认线程池状态的成员变量。
代码如下:
//thread_pool.hpp
class ThreadPool {
private:
//任务队列,存储任务
std::priority_queue<Job> _task_queue{};
//线程列表,存储线程
std::vector<std::thread> _thread_list{};
//锁对象,用于保证线程安全
std::mutex _thread_lock{};
//条件变量,和_thread_lock配合使用,用于控制线程间的同步
std::condition_variable _cv;
//用于确定线程池是否停止,使用atomic包装保证线程按安全
std::atomic<bool> _is_stop{false};
//用于确认线程池是否已经开启
bool _is_started{false};
//线程池中线程的数量
size_t _thread_num{0};
};
(2)构造函数
因为线程池我们会采用单例模式,所以移动构造、拷贝构造以及赋值重载的函数我们都不需要,只需要一个普通的初始化构造函数即可。
//thread_pool.hpp
class ThreadPool {
private:
//任务队列,存储任务
std::priority_queue<Job> _task_queue{};
//线程列表,存储线程
std::vector<std::thread> _thread_list{};
//锁对象,用于保证线程安全
std::mutex _thread_lock{};
//条件变量,和_thread_lock配合使用,用于控制线程间的同步
std::condition_variable _cv;
//用于确定线程池是否停止,使用atomic包装保证线程按安全
std::atomic<bool> _is_stop{false};
//用于确认线程池是否已经开启
bool _is_started{false};
//线程池中线程的数量
size_t _thread_num{0};
public:
//把不需要的构造函数delete
ThreadPool(ThreadPool &&other) = delete;
ThreadPool(const ThreadPool &other) = delete;
void operator=(ThreadPool&&) = delete;
//传入线程池初始化时的线程数量
ThreadPool(size_t thread_num);
};
//对ThreadPool()的实现
ThreadPool::ThreadPool(size_t thread_num) : _thread_num{thread_num} {}
(3)线程执行逻辑函数
该函数可以说是整个线程池执行任务的核心逻辑了,首先需要明白的是线程池中的线程执行任务的逻辑。线程池中的线程会循环向任务队列(_task_queue
)中去获取任务来执行,虽然之前我们把任务做了许多封装,但是在线程中,任务其实可以简单的看成是一个简单的“函数”,而线程所做的就是不断循环的去任务队列中获取并执行这些“函数”。接下来是一些需要注意的细节:
- 当线程获取从任务队列中获取任务时,需要加锁以保障线程安全
- 当任务队列为空时要使用条件变量(
condition_variable
)来让该线程陷入休眠,使其让出cpu,防止陷入忙等且能提高效率。但是当任务队列不为空或线程池停止时,需要立刻唤醒休眠中的线程。- 当获取到任务时就应该放开锁,之后在执行任务。
- 我们将该执行逻辑封装成一个lambda函数,并且使用
_get_execute_func
这个函数将其返回(这个函数我们设为私有)。
代码如下:
//thread_pool.hpp
//ThreadPool
//private
std::function<void()> _get_execute_func();
std::function<void()> ThreadPool::_get_execute_func() {
return [&]() {
//不断从任务队列中循环获取任务,直到线程池停止运行
while (true) {
// 如果线程池停止了,直接退出
if (_is_stop) return;
// 获取任务时,先加锁保证线程安全
std::unique_lock<std::mutex> lock(_thread_lock);
// 如果任务队列为空就一直循环
while (_task_queue.empty()) {
// 任务队列为空时,让线程让出cpu并等待,直到任务队列不为空或线程池停止时唤醒
_cv.wait(lock, [&] { return !_task_queue.empty() || _is_stop; });
// 如果线程池停止了,直接退出
if (_is_stop) return;
}
// 获取任务队列中的任务
auto job = _task_queue.top();
// 获取完任务后要立即将其从列表中移除
_task_queue.pop();
// 获取完任务后要放开锁
lock.unlock();
try {
// 执行任务中的任务函数(即开始执行任务)
job._task._execute_task();
// 抓一下异常
} catch (const std::exception &e) {
std::cerr << "Caught an exception in ThreadPool: " << e.what() << std::endl;
} catch (...) {
std::cerr << "Caught an unknown exception in ThreadPool." << std::endl;
}
}
};
}
(4)不安全的添加线程的函数
接下来编写向线程池中添加线程的函数(不安全)。这里的不安全是指线程不安全,也就是说在向线程池中添加线程的过程中并没有加锁。虽然不安全,但是效率更高,主要是作为在线程池启动时用于创建线程的工具函数,所以要将其设为私有。
代码如下:
//thread_pool.hpp
//ThreadPool
//private
void _add_thread_unsafe(size_t thread_num);
void ThreadPool::_add_thread_unsafe(size_t thread_num) {
//按照指定的线程数量向线程数组中创建线程,执行的逻辑从我们之前编写的_get_execute_func()中去获取
for (int i = 0; i < thread_num; i++) {
auto execute_func = _get_execute_func();
_thread_list.emplace_back(execute_func);
}
}
(5)安全的添加线程的函数
该函数因为加了锁线程安全,所以主要用于在线程池运行过程中来向线程数组中添加线程。所以与上一个添加线程的函数不同的一点是,该函数在添加线程时,也需要将成员变量_thread_num
的值增加。
代码如下:
//thread_pool.hpp
//ThreadPool
//public
void add_thread_safe(size_t thread_num);
// 指定要添加线程的数量
void ThreadPool::add_thread_safe(size_t thread_num) {
//加锁保障线程安全
std::unique_lock<std::mutex> lock(_thread_lock);
_thread_num += thread_num;
// 添加线程
for (int i = 0; i < thread_num; i++) {
auto execute_func = _get_execute_func();
_thread_list.emplace_back(std::move(execute_func));
}
}
(6)添加任务
编写向任务队列中添加任务的函数,这里我们同样设置两个版本,一个线程安全(主要用于在线程池启动后添加任务),一个线程不安全(效率更高,主要用于在线程池启动前添加任务)
代码如下:
//thread_pool.hpp
//ThreadPool
//public
// 线程安全的添加任务
void add_task(std::function<void()> &&task, int priority);
// 线程不安全的添加任务
void add_task_unsafe(std::function<void()> &&task, int priority);
//传入需要执行的任务,形式为function的右值(可以以lambda的形式传入)
//指定任务的优先级,默认为0
void ThreadPool::add_task(std::function<void()> &&task, int priority = 0) {
//加锁保障线程安全
std::unique_lock<std::mutex> lock(_thread_lock);
_task_queue.push({{std::move(task), priority}});
}
void ThreadPool::add_task_unsafe(std::function<void()> &&task, int priority = 0) {
_task_queue.push({{std::move(task), priority}});
}
(7)启动线程池的函数
启动线程池的函数逻辑较为简单,主要就是向线程数组中添加指定数量的线程并启动线程,然后设置一下线程池的状态。
//thread_pool.hpp
//ThreadPool
//public
void start();
void ThreadPool::start() {
// 如果线程池已经启动,则抛出异常
if (_is_started) throw std::runtime_error("the thread pool already started...");
_is_started = true;
// 添加线程,这里可以采用不安全的形式
_add_thread_unsafe(_thread_num);
}
(8)线程池启动后同步阻塞
用于等待线程池中所有的线程join
结束,可以用于同步。
//thread_pool.hpp
//ThreadPool
//public
void sync();
void ThreadPool::sync() {
// 遍历所有线程,等待线程结束,可以用于同步
std::for_each(_thread_list.begin(), _thread_list.end(), [](std::thread &t) { t.join(); });
}
(9)线程池强制关闭
我们可以通过把线程池里的所有线程从主线程中强制分离(detach
)出去,这样主线程就不会等待子线程join而直接向下运行。因此,使用强制关闭可以使线程池中的线程立即停止,也就意味着即使工作线程里有任务正在执行,主线程也不会等待任务执行完成。
//thread_pool.hpp
//ThreadPool
//public
void force_stop();
void ThreadPool::force_stop() {
_is_started = false;
// 如果线程已经停止,则抛出异常
if (_is_stop) throw std::runtime_error("thread pool already shutdown!");
// 将线程池中的线程全部从主线程中分离,以强制停止线程池
for (auto& thread : _thread_list) {
thread.detach();
}
}
(10)线程池优雅停止
编写能够使线程池优雅停止的函数,该函数不会使线程池立即停止,而是等所有线程都完成了自己正在执行的任务之后才会停止。该函数保证了不会有任务只执行了一半就中途结束线程,更加安全。
//thread_pool.hpp
//ThreadPool
//public
void force_stop_gracefully();
void ThreadPool::force_stop_gracefully() {
_is_started = false;
// 将停止标记设置为true
_is_stop = true;
// 同步等待线程真正结束
this->sync();
}
(11)析构函数
析构函数我们只需要简单的调用一下force_stop_gracefully
即可。
//thread_pool.hpp
//ThreadPool
//public
~ThreadPool();
ThreadPool::~ThreadPool() {
if (!_is_stop) force_stop_gracefully();
}
(12)单例的获取
我们可以通过编写一个函数来获取线程池的单例。在函数中存放一个线程池类的静态变量,然后返回它的左值引用。因为在C++中,函数中的静态变量只会被执行初始化一次,所以当多次调用该函数时,返回的实际上都是同一个对象,可以通过这个特性来实现饿汉单例。
//thread_pool.hpp
ThreadPool& get_thread_pool(size_t thread_num) {
static ThreadPool pool{thread_num};
return pool;
}
(13)线程池状态的获取
就是一些简单的get函数,来获取线程池的一些状态。
//thread_pool.hpp
//ThreadPool
//public
//获取线程池中的当前线程数量
[[nodiscard]] size_t get_cur_thread_num() const;
//获取当前线程池中的任务队列大小
[[nodiscard]] size_t get_task_queue_size();
size_t ThreadPool::get_cur_thread_num() const {
return _thread_num;
}
size_t ThreadPool::get_task_queue_size() {
return this->_task_queue.size();
}
至此,线程池所有的代码编写完毕,接下来就可以在main.cpp中编写main函数来进行测试。
四、测试
测试主要是测试两个点,一是任务的执行是否实现了并发执行,二是任务的优先级是否生效,三是优雅停机是否生效。
我们可以首先指定8个初始工作线程,然后执行100个任务,然后我们将第56个任务的优先级调高,看它是否会被优先执行。测试代码如下:
// main.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include "thread_pool.hpp"
int main() {
std::cout << "start" << std::endl;
//获取线程池
auto& pool = get_thread_pool(8);
//向线程池中添加100个任务
for (int i = 0; i < 100; i++) {
// 第56个任务调高其优先级
if (i == 56) {
pool.add_task_unsafe([=] {
{ //休眠一秒模拟任务耗时
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << i << std::endl;
}
}, 3);
continue;
}
pool.add_task_unsafe([=] {
{
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << i << std::endl;
}
});
}
// 启动线程池
pool.start();
//阻塞8秒后优雅停机
std::this_thread::sleep_for(std::chrono::seconds(8));
pool.force_stop_gracefully();
return 0;
}
最终测试结果符合预期。
五、总结
线程池本质是一种池化技术,通过固定的线程数量来处理任务,而不会频繁的开辟和释放线程,大大的提高了程序运行的效率,尤其是在io密集型的任务中先得尤为重要。
ThreadPool.hpp的完整代码如下:
// thread_pool.hpp
#include <iostream>
#include <thread>
#include <utility>
#include <vector>
#include <functional>
#include <queue>
#include <cassert>
#include <mutex>
#include <condition_variable>
struct Task {
int _priority{0};
std::function<void()> _execute_task;
Task() = default;
Task(std::function<void()> &&task) : _execute_task{task} {}
Task(std::function<void()> &&task, int priority) : _execute_task{task}, _priority{priority} {}
};
struct Job {
Task _task{};
time_t _creat_time{};
bool operator<(const Job &other) const;
Job(Task task) : _task{std::move(task)} {
this->_creat_time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
}
};
bool Job::operator<(const Job &other) const {
return this->_task._priority == other._task._priority ? this->_creat_time < other._creat_time :
this->_task._priority < other._task._priority;
}
class ThreadPool {
private:
std::priority_queue<Job> _task_queue{};
std::vector<std::thread> _thread_list{};
std::mutex _thread_lock{};
std::condition_variable _cv;
std::atomic<bool> _is_stop{false};
bool _is_started{false};
size_t _thread_num{0};
std::function<void()> _get_execute_func();
void _add_thread_unsafe(size_t thread_num);
public:
ThreadPool(ThreadPool &&other) = delete;
ThreadPool(const ThreadPool &other) = delete;
void operator=(ThreadPool&&) = delete;
void start();
void force_stop_gracefully();
void force_stop();
void add_task(std::function<void()> &&task, int priority);
void add_task_unsafe(std::function<void()> &&task, int priority);
void sync();
void add_thread_safe(size_t thread_num);
[[nodiscard]] size_t get_cur_thread_num() const;
[[nodiscard]] size_t get_task_queue_size();
~ThreadPool();
ThreadPool(size_t thread_num);
};
ThreadPool::ThreadPool(size_t thread_num) : _thread_num{thread_num} {}
void ThreadPool::start() {
if (_is_started) throw std::runtime_error("the thread pool already started...");
_is_started = true;
_add_thread_unsafe(_thread_num);
}
void ThreadPool::add_task(std::function<void()> &&task, int priority = 0) {
std::unique_lock<std::mutex> lock(_thread_lock);
_task_queue.push({{std::move(task), priority}});
}
void ThreadPool::add_task_unsafe(std::function<void()> &&task, int priority = 0) {
_task_queue.push({{std::move(task), priority}});
}
void ThreadPool::sync() {
std::for_each(_thread_list.begin(), _thread_list.end(), [](std::thread &t) { t.join(); });
}
void ThreadPool::force_stop_gracefully() {
_is_started = false;
_is_stop = true;
this->sync();
}
ThreadPool::~ThreadPool() {
if (!_is_stop) force_stop_gracefully();
}
size_t ThreadPool::get_task_queue_size() {
return this->_task_queue.size();
}
void ThreadPool::_add_thread_unsafe(size_t thread_num) {
for (int i = 0; i < thread_num; i++) {
auto execute_func = _get_execute_func();
_thread_list.emplace_back(execute_func);
}
}
void ThreadPool::add_thread_safe(size_t thread_num) {
std::unique_lock<std::mutex> lock(_thread_lock);
_thread_num += thread_num;
for (int i = 0; i < thread_num; i++) {
auto execute_func = _get_execute_func();
_thread_list.emplace_back(std::move(execute_func));
}
}
std::function<void()> ThreadPool::_get_execute_func() {
return [&]() {
while (true) {
if (_is_stop) return;
std::unique_lock<std::mutex> lock(_thread_lock);
while (_task_queue.empty()) {
_cv.wait(lock, [&] { return !_task_queue.empty() || _is_stop; });
if (_is_stop) return;
}
auto job = _task_queue.top();
_task_queue.pop();
lock.unlock();
try {
job._task._execute_task();
} catch (const std::exception &e) {
std::cerr << "Caught an exception in ThreadPool: " << e.what() << std::endl;
} catch (...) {
std::cerr << "Caught an unknown exception in ThreadPool." << std::endl;
}
}
};
}
size_t ThreadPool::get_cur_thread_num() const {
return _thread_num;
}
void ThreadPool::force_stop() {
_is_started = false;
if (_is_stop) throw std::runtime_error("thread pool already shutdown!");
for (auto& thread : _thread_list) {
thread.detach();
}
}
ThreadPool& get_thread_pool(size_t thread_num) {
static ThreadPool pool{thread_num};
return pool;
}