QThreadPool 源码剖析
Qt 版本:5.15.2
QThreadPool 类
先大致了解下 QThreadPool 提供了哪些接口和功能,代码位于 qthreadpool.h 文件中。
class QThreadPool
{
public:
static QThreadPool *globalInstance();
// 如果有空闲线程则直接开始任务,如果没有空闲线程,则返回false
bool tryStart(QRunnable *runnable);
bool tryStart(std::function<void()> functionToRun);
// 如果有空闲线程则直接开始任务,如果没有空闲线程,则将任务入到队列中排队
void start(QRunnable *runnable, int priority = 0);
void start(std::function<void()> functionToRun, int priority = 0);
// 线程过期时间,当线程处于空闲状态的时长超过该值时,线程退出。默认30000毫秒
int expiryTimeout() const;
void setExpiryTimeout(int expiryTimeout);
// 最大线程数量,默认为CPU核心数
int maxThreadCount() const;
void setMaxThreadCount(int maxThreadCount);
// 活动线程的数量,即处于忙碌中的线程数量
int activeThreadCount() const;
// 线程堆栈大小
void setStackSize(uint stackSize);
uint stackSize() const;
// 多预留一个线程,这将使得线程池可以突破maxThreadCount()限制,扩建出更多活动线程
void reserveThread();
void releaseThread();
// 等待所有任务被完成
bool waitForDone(int msecs = -1);
// 清空任务队列,已开始的任务无法被清
void clear();
// 判断某个线程是不是属于当前线程池
bool contains(const QThread *thread) const;
// 尝试从队列中移除指定任务,仅能移除还在队列中排队的任务
bool tryTake(QRunnable *runnable);
};
可以看到该类中没有声明成员变量,成员变量被声明在了 QThreadPoolPrivate 类中。QThreadPool 就是一个壳,真正的实现位于 QThreadPoolPrivate 类中。
QThreadPoolPrivate 类
该类是线程池的真正实现类,代码位于 qthreadpool_p.h 文件中
// 类的成员变量
class QThreadPoolPrivate
{
public:
mutable QMutex mutex;
QSet<QThreadPoolThread *> allThreads; // 全部线程句柄(waitingThreads + expiredThreads)
QQueue<QThreadPoolThread *> waitingThreads; // 处于等待中的空闲线程句柄
QQueue<QThreadPoolThread *> expiredThreads; // 已过期线程句柄,这些线程已退出不再运行,需要重新start创建线程才能使用
QWaitCondition noActiveThreads; // 没有活动线程时触发,表示大伙都闲下来了,没有任务了
QVector<QueuePage*> queue; // 任务队列,存储了所有尚未执行,还在排队中的任务
int expiryTimeout = 30000;
int maxThreadCount = QThread::idealThreadCount();
int reservedThreads = 0; // 多预留出的线程数
int activeThreads = 0; // 活动线程的数量,即处于忙碌中的线程数量
uint stackSize = 0;
};
线程池的功能实现,是基于对以上成员变量的操作,这里我们先从成员变量入手,反推其接口都干了啥。成员变量中引入了两个新类型:
- QThreadPoolThread
- QueuePage
下面再分别了解下这两个类型的作用。
QThreadPoolThread 类
这个类非常简单,就继承并重写了 QThread::run,做为消费者,一直在后台等待生产者投递任务过来,然后执行任务,然后再次进入等待。它的声明和实现都在 qthreadpool.cpp 文件中。
// 继承并重写了线程入口函数
class QThreadPoolThread : public QThread
{
public:
QThreadPoolThread(QThreadPoolPrivate *manager);
void run() override;
void registerThreadInactive();
// 以下是成员变量======================================
QWaitCondition runnableReady; // runnable变量可用时触发
QThreadPoolPrivate *manager; // 记录了它所属的线程池
QRunnable *runnable; // 该线程所携带的任务句柄
};
// 注册线程为非活动线程
// 该函数的调用时机:线程空闲下来后
// 其本质就是将线程池中,活动线程数量减1
void QThreadPoolThread::registerThreadInactive()
{
if (--manager->activeThreads == 0)
manager->noActiveThreads.wakeAll();
}
// 线程入口函数
void QThreadPoolThread::run()
{
QMutexLocker locker(&manager->mutex); // 加锁父线程池
for(;;) {
// 将成员变量runnable的值,移动到临时变量r中,即取得本次任务的句柄
QRunnable *r = runnable;
runnable = nullptr;
do {
// 如果任务句柄不为空,则要开始执行任务了
if (r) {
const bool del = r->autoDelete();
locker.unlock(); // 解锁父线程池
// <PRE> <<<<<<<<<<<<<<<< 以下是未加锁区域 <<<<<<<<<<<<<<<<
r->run(); // 执行任务
if (del)
delete r; // 销毁任务句柄
locker.relock(); // 重新加锁父线程池
// </PRE> >>>>>>>>>>>>>>>> 以上是未加锁区域 >>>>>>>>>>>>>>>
}
// 当活动线程数量达到上限时,当前线程需要退出循环并结束掉
if (manager->tooManyThreadsActive())
break;
// 如果任务队列为空了,当前线程需要切换为空闲线程
if (manager->queue.isEmpty()) {
r = nullptr;
break;
}
// 从队列取出下一个任务句柄,准备继续执行
QueuePage *page = manager->queue.first();
r = page->pop();
// 如果队列已经被取空了,则销毁
if (page->isFinished()) {
manager->queue.removeFirst();
delete page;
}
} while (true);
// 上面的while死循环,退出条件有二
// 1. 当活动线程数量达到上限时
// 2. 任务队列为空
// 否则正常从任务队列中取出下一个任务并执行。
bool expired = manager->tooManyThreadsActive();
if (!expired) {
// 通过上面逻辑可得,走进此分支的条件是:任务队列为空,且活动线程数量未达上限
manager->waitingThreads.enqueue(this); // 将当前线程记录到空闲等待线程中
registerThreadInactive(); // 注册当前线程为非活动线程,即将父线程池的活动线程数量减1
// 进入超时等待,此时会解锁父线程池,并开始等待被唤醒
runnableReady.wait(locker.mutex(), QDeadlineTimer(manager->expiryTimeout));
// 等待结束,有可能是有任务来了,也有可能是超时了
// 先将父线程池中的活动线程数量加1
++manager->activeThreads;
// 尝试将自己从空闲线程中移除
// 如果移除成功,表示自己一直在空闲状态,到过期时间了,需要退出了
// 如果移除失败,表示自己是被任务唤醒的,自己已经被线程池从空闲队列中踢出来了
// 移除失败有两种情况:
// 1. 有任务来了,需要开始执行任务了
// 2. 用户调用了waitForDone清空类中存储的所有线程句柄
if (manager->waitingThreads.removeOne(this))
expired = true;
// 如果全部线程句柄中并没有自己,则将自己注册为非活动线程并退出,结束线程
if (!manager->allThreads.contains(this)) {
// ???什么场景下,线程池中会不包含当前线程句柄???
// 当有人调用waitForDone后,线程池中的所有线程句柄都已被清空
registerThreadInactive();
break; // 退出for循环,即结束线程
}
} // if (!expired)
if (expired) {
// 根据上面代码逻辑可得,要走进当前代码分支,有以下几个时机:
// 1. 当活动线程数量达到上限时
// 2. 当线程一直处于空闲态,到达过期时间后
// 此时线程即将退出,不再可用
manager->expiredThreads.enqueue(this);
registerThreadInactive();
break; // 退出for循环,即结束线程
}
} // for (;;)
}
QueuePage 类
任务队列的封装,它的声明和实现位于 qthreadpool_p.h 文件中。
class QueuePage {
public:
enum {
MaxPageSize = 256
};
void push(QRunnable *runnable) {
m_lastIndex += 1;
m_entries[m_lastIndex] = runnable;
}
QRunnable *pop() {
QRunnable *runnable = first();
m_entries[m_firstIndex] = nullptr;
m_firstIndex += 1;
// 此处省略了部分源码......
return runnable;
}
int priority() const {
return m_priority;
}
private:
int m_priority = 0;
int m_firstIndex = 0;
int m_lastIndex = -1;
QRunnable *m_entries[MaxPageSize];
};
可以看出,QueuePage 本质就是一个数组,最大为256,里面放的全是同一优先级的任务。
此时再结合 QThreadPoolPrivate 的成员变量一起:
class QThreadPoolPrivate
{
public:
QVector<QueuePage*> queue; // 任务队列,存储了所有尚未执行,还在排队中的任务
};
可以看出来,队列是一个数组套数组的结构,再结合一些内部逻辑代码,最终我们得到如图所示的线程池队列结构:
QVector<QueuePage*> 是一个有序数组,按 m_priority 大小排序。
- 入队时:
- 在 QVector 中找到对应优先级的任务队列,并将其追加到队尾
- 如果 QVector 中不存在该优先级,则新建并入队。
- 如果 QRunnable[256] 队伍已满,则再新建一个 QueuePage 队伍并入队。此时两支队伍的 m_priority 值相同。
- 出队时:
- 按优先级顺序出队,高优先级的先出队。
到这里基本就差不多了,最后再看下线程池的添加任务接口,是如何将任务添加到线程池中执行的。
QThreadPool::start 函数
// 新建一个后台线程,并立即执行任务
void QThreadPoolPrivate::startThread(QRunnable *runnable)
{
QScopedPointer <QThreadPoolThread> thread(new QThreadPoolThread(this));
allThreads.insert(thread.data()); // 记录线程句柄到线程池
++activeThreads; // 线程池中的活动线程数量加1
// 直接将任务赋值给该线程,并启动该线程(不用将任务入队)
thread->runnable = runnable;
thread.take()->start(); // 内部调用_beginthreadex/CreateThread等API创建后台线程
}
// 如果有空闲线程,则直接开始任务,如果没有空闲线程则返回失败
bool QThreadPoolPrivate::tryStart(QRunnable *task)
{
if (allThreads.isEmpty()) {
// 如果线程池中一个线程都没有,则直接新建一个线程并执行任务
startThread(task);
return true;
}
// 线程数量已达上限,失败返回
if (activeThreadCount() >= maxThreadCount)
return false;
if (waitingThreads.count() > 0) {
// 有空闲线程在等待,先将任务入队,再取出一个空闲的线程,唤醒它领取任务
enqueueTask(task);
waitingThreads.takeFirst()->runnableReady.wakeOne();
return true;
}
if (!expiredThreads.isEmpty()) {
// 执行到这里,说明
// 1. 线程数量不为空,且未达上限
// 2. 没有空闲可用的线程
// 3. 有已过期的线程,可以回收再利用
// 此时回收再利用一下过期的线程即可
QThreadPoolThread *thread = expiredThreads.dequeue();
++activeThreads;
thread->runnable = task;
thread->start(); // 过期的线程,其线程已经不存在了,需要重新创建线程
return true;
}
// 执行到这里说明:
// 1. 线程数量不为空,且未达上限
// 2. 没有空闲可用的线程
// 3. 没有已过期的线程可回收再利用
// 此时需要新建一个线程,并直接将任务直接丢给它去完成
startThread(task);
return true;
}
void QThreadPool::start(QRunnable *runnable, int priority)
{
// 先tryStart,如果失败,则需要将任务插入到队列中排队等待
if (!d->tryStart(runnable)) {
d->enqueueTask(runnable, priority);
// 如果有空闲线程,则取出一个空闲的线程,唤醒它领取任务
if (!d->waitingThreads.isEmpty())
d->waitingThreads.takeFirst()->runnableReady.wakeOne();
}
}
QThreadPool::tryTake 函数
如何取消任务?先看代码:
class QThreadPool
{
public:
// 尝试从队列中移除指定任务,仅能移除还在队列中排队的任务
bool tryTake(QRunnable *runnable);
};
QThreadPool 只能取消一个还在队列排队,尚未被执行的任务。如果想要实现取消正在运行的任务,需要自己添加额外的接口。它的实现也非常的简单,就是遍历队列,找到与之匹配的任务,从队列中将其移除。
QThreadPoll::waitForDone 函数
如果等待任务全部结束?先看代码:
bool QThreadPool::waitForDone(int msecs)
{
return d->waitForDone(msecs); // 实际调用的QThreadPoolPrivate::waitForDone(int)
}
bool QThreadPoolPrivate::waitForDone(int msecs)
{
QMutexLocker locker(&mutex);
QDeadlineTimer timer(msecs);
do {
// 当超时后还有任务在跑,则返回失败
if (!waitForDone(timer))
return false;
reset();
// More threads can be started during reset(), in that case continue
// waiting if we still have time left.
} while ((!queue.isEmpty() || activeThreads) && !timer.hasExpired());
return queue.isEmpty() && activeThreads == 0;
}
bool QThreadPoolPrivate::waitForDone(const QDeadlineTimer &timer)
{
// noActiveThreads的触发时机是:没有活动线程,大伙都闲下来时
// 只要队列还有任务,或活动线程数量不为零,就一直等待,直到超时
while (!(queue.isEmpty() && activeThreads == 0) && !timer.hasExpired())
noActiveThreads.wait(&mutex, timer);
return queue.isEmpty() && activeThreads == 0;
}
void QThreadPoolPrivate::reset()
{
// move the contents of the set out so that we can iterate without the lock
QSet<QThreadPoolThread *> allThreadsCopy;
allThreadsCopy.swap(allThreads);
expiredThreads.clear();
waitingThreads.clear();
mutex.unlock(); // 上面的代码都还在上锁区域内,下面的区域,未加锁(因为需要耗时等待)
// 逐个唤醒任务
// 这里可以结合QThreadPoolThread::run函数一起看
// 任务一旦唤醒,它会尝试将自己从allThreads成员中移除
// allThreads已被置换清空,所以会移除失败,致使线程走进退出流程
for (QThreadPoolThread *thread: qAsConst(allThreadsCopy)) {
if (!thread->isFinished()) {
thread->runnableReady.wakeAll();
thread->wait();
}
delete thread;
}
mutex.lock();
}
总结
QThreadPool 的动态分配做的很好,可以在零和最大值区间内很好的控制线程数量。过期线程的清理也都很及时。
同时它没有提供取消正在运行的线程接口,需要我们自己去实现。