QThreadPool 源码剖析

252 阅读9分钟

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; // 任务队列,存储了所有尚未执行,还在排队中的任务
};

可以看出来,队列是一个数组套数组的结构,再结合一些内部逻辑代码,最终我们得到如图所示的线程池队列结构:

QueuePage.jpg

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 的动态分配做的很好,可以在零和最大值区间内很好的控制线程数量。过期线程的清理也都很及时。

同时它没有提供取消正在运行的线程接口,需要我们自己去实现。