SoftGLRender源码:线程池ThreadPool类
特性
文件:Base/ThreadPool.h
线程池:预先创建一组线程(对象),用于并发完成用户指定任务的机制,避免频繁创建、销毁线程,而降低效率.
关键点:
- 如何创建这组线程?何时创建?
- 线程组的子线程如何运行?
- 如何维护用户任务?
- 线程组如何取出并执行用户任务?如何传递参数给用户任务?
- 如何终止、销毁线程池?
- 如何确保线程安全?
线程池模型
典型线程池模型:

ThreadPool数据成员
ThreadPool包含哪些数据成员?
- 一组线程对象
threads_及其数量threadCnt_,用于执行用户任务; - 一个任务队列
tasks_及其数量tasksCnt_,包含用户要执行的任务; - 互斥锁
mutex_,确保对用户队列的操作的线程安全.
public:
...
// 暂停子线程,用于控制是否暂停正在执行用户任务的子线程
std::atomic<bool> paused{ false };
private:
mutable std::mutex mutex_ = {};
// 线程池运行状态,用于控制是否退出线程池内部工作循环
std::atomic<bool> running_{ true };
// 工作线程数组
std::unique_ptr <std::thread[]> threads_;
// 工作线程组包含线程数量
std::atomic<size_t> threadCnt_{ 0 };
// 任务队列,由用户传入,工作线程组执行
std::queue<std::function<void(size_t)>> tasks_ = {};
// 任务队列中任务数量
std::atomic<size_t> tasksCnt_{ 0 };
ThreadPool实现
线程池的构造、析构
线程池构造时,创建工作线程组;析构时,等待用户任务完成后,销毁线程(连接线程).
tips: 子线程的销毁(连接),必须在用户任务执行完毕后,否则可能造成用户任务的异常.
class ThreadPool {
public:
explicit ThreadPool(const size_t threadCnt = std::thread::hardware_concurrency())
: threadCnt_(threadCnt), threads_(new std::thread[threadCnt]) {
createThreads(); // 创建线程组
}
~ThreadPool() {
waitTasksFinish(); // 等待所有用户任务执行完
running_ = false;
joinThreads(); // 连接所有子线程
}
...
};
创建线程组
ThreadPool用C++ 11引入的std::thread创建、管理线程;用std::unique_ptr<std::thread[]>来管理子线程数组threads_,默认子线程数量为逻辑核心数std::thread::hardware_concurrency()(非物理核心数).
class ThreadPool {
public:
explicit ThreadPool(const size_t threadCnt = std::thread::hardware_concurrency())
: threadCnt_(threadCnt), threads_(new std::thread[threadCnt]) {
createThreads();
}
...
private:
void createThreads() {
for (size_t i = 0; i < threadCnt_; i++) {
threads_[i] = std::thread(&ThreadPool::taskWorker, this, i);
}
}
...
}
创建std::thread对象时,传递成员函数ThreadPool::taskWorker的指针作为参数,但编译器并不知道该函数属于哪个对象,因此,必须传递对象指针,即this. taskWorker是子线程的线程函数.
销毁线程组
并不能通过直接销毁线程池对象,而销毁线程池,因为线程池还持有线程组资源、用户任务资源. 必须执行完所有用户任务后,再统一销毁所有子线程,避免用户任务出现异常.
因此,析构函数中设置waitTasksFinish(),等待用户任务完成.
public:
~ThreadPool() {
waitTasksFinish();
running_ = false;
joinThreads();
}
void waitTasksFinish() const { // 等待执行用户任务完成
while (true) {
if (!paused) {
if (tasksCnt_ == 0) {
break;
}
else {
if (tasksRunningCnt() == 0) {
break;
}
// 暂停当前线程,即析构ThreadPool对象的线程
std::this_thread::yield();
}
}
}
}
private:
size_t tasksRunningCnt() const { // 当前正在执行的用户任务数
// 正在执行的用户任务数 = 总任务数 - 队列中待执行任务数
return tasksCnt_ - tasksQueueCnt();
}
void joinThreads() { // 连接所有子线程,系统回收其资源
for (size_t i = 0; i < threadCnt_; i++) {
threads_[i].join();
}
}
向任务队列添加用户任务
线程池的目的是提供一组线程,执行用户任务,需要为用户提供添加待执行用户任务接口.
使用std::queue(FIFO队列)管理用户任务,特点是队列尾插入数据、队列头取出数据,遵循FIFO规则.
public:
// 入队列
// 添加用户任务,不带参数
// 模板函数接受任何可调用对象,作为参数task,代表用户任务
// F是模板参数,也是函数类型,由于内部转型,F对应实参必须是形如 void(size_t) 的可调用对象
template<typename F>
void pushTask(const F& task) {
tasksCnt_++;
{
const std::lock_guard<std::mutex> lock(mutex_); // 线程安全
// task转型,否则可能无法通过编译
tasks_.push(std::function<void(size_t)>(task));
}
}
template<typename F, typename... A> // 模板参数包
void pushTask(const F& task, const A &...args) { // 函数参数包
pushTask([task, args...] { task(args...); }); // 包扩展
}
e.g. 客户端向线程池添加任务(调用第1个pushTask()):
ThreadPool pool(6);
pool.pushTask([&](int thread_id) { skyboxTex[0] = loadTextureFile(filepath + "right.jpg"); });
...
这里实参是形如 [&](int thread_id) {...}的lambda.
关于变长模板函数pushTask的定义,个人认为是有问题的,因为:
- 没有定义终止条件;
- 变长版本
pushTask中调用的pushTask是递归版本,还是非递归版本?
如果前者,由于前面我们已经知道,模板参数F对应实参,必须是形如void(size_t)的lambda,而这里,显然没有提供;
如果后者,实参[task, args...] { task(args...); }显然只是单个lambda,并没有后面的参数.
- 不像第1个版本,没有明确指示模板参数
F对应的类型是什么.
因为程序并未使用该变长参数版本,因此不深究.
从任务队列取出用户任务
用户并不直接从任务队列取出任务,只有线程组才会取,因此接口设计为private.
private:
// 出队列
bool popTask(std::function<void(size_t)>& task) {
const std::lock_guard<std::mutex> lock(mutex_);
if (tasks_.empty()) {
return false;
}
else{
task = std::move(tasks_.front());
tasks_.pop();
return true;
}
}
为什么这里使用std::move,而不是直接拷贝?
因为为了避免不必要的拷贝开销,提高效率. 队列tasks_存放元素类型std::function<void(size_t)>,如果直接赋值,则会拷贝整个对象,而std::function本质是一个函数包装器,内部可能包含堆分配(如捕获大量变量),造成很大开销.
子线程执行用户任务
子线程组自动从任务队列取出用户任务,并执行,是线程池内部自动完成的,无需外部参与.
taskWorker是每个子线程的线程函数,线程id是createThreads创建线程组时传入的顺序编号(1,2,...,n),并非OS分配的. 传入线程id是为了方便调试.
private:
// 子线程调用的工作线程
// 维护一个主循环:不断取出用户任务并执行
void taskWorker(size_t threadId) {
while (running_) {
std::function<void(size_t)> task;
if (!paused && popTask(task)) { // 从队列取出用户任务
task(threadId); // 执行用户任务
tasksCnt_--;
}
else {
std::this_thread::yield();
}
}
}
任务队列空时,如何处理?
线程池子线程由于取不到用户任务,因此处于空闲状态,可以主动让出(yield())CPU资源.
子线程的暂停与退出
-
暂停:子线程通过
paused实现线程暂停,暂停执行任务,主动让出CPU资源; -
退出:子线程通过
running_实现退出线程的主循环. 子线程退出后,才能被主线程连接,以回收线程资源;否则,未执行完的用户任务,可能产生异常.
线程安全
要什么会涉及线程安全?为什么只给队列操作pushTask(插入任务),popTask(弹出任务)加锁?
因为主线程创建线程池后,运行在主线程的用户,会往任务队列添加用户任务,而子线程会不断从队列取出任务. 也就是说,只有这2个操作时,存在不同线程同时访问队列的情形,因此需要用互斥锁mutex_确保线程安全.

浙公网安备 33010602011771号