前言
本节将介绍Qt中最后一种线程使用方式,QThreadPool + QRunnable,也就是线程池。线程池相比于普通单线程来说,最大的区别就是批量线程的调度。
QThreadPool 是 Qt 提供的线程池管理器,它可以复用线程、控制最大并发线程数、自动回收空闲线程。
QRunnable是一个可运行任务的抽象基类,需要继承并自行实现。
一、代码示例
这里做一个最简单的例子:
#ifndef PRINTTASK_H
#define PRINTTASK_H
#include <QThreadPool>
#include <QRunnable>
#include <QDebug>
// 定义任务类
class PrintTask : public QRunnable
{
public:
PrintTask(int id) : m_id(id) {}
~PrintTask(){ qDebug()<<"~PrintTask"<<m_id; }
// 必须重写 run()
void run() override
{
qDebug() << "【任务" << m_id << "】开始执行,线程 ID:" << QThread::currentThreadId();
QThread::sleep(2); // 模拟耗时操作
qDebug() << "【任务" << m_id << "】执行完毕";
// 注意:不能在这里直接操作 GUI!
// 如果需要更新 UI,应通过信号或 QMetaObject::invokeMethod 回到主线程
}
private:
int m_id;
};
#endif // PRINTTASK_H
这是一个继承自QRunnable的打印类,主要就是信息打印加延时。
然后就是按钮触发的参函数,直接获取线程池全局句柄,并创建任务类,让线程池启动它:
void MainWindow::on_btn_thread_start_4_clicked()
{
// 获取全局线程池
QThreadPool* pool = QThreadPool::globalInstance();
qDebug() << "最大线程数:" << pool->maxThreadCount();
// 提交 3 个任务
for (int i = 1; i <= 3; ++i) {
PrintTask* task = new PrintTask(i);
//️ 关键:不要 delete task!线程池会自动删除
pool->start(task);
}
}
可以看到,代码实际上非常简单,它甚至不需要我们析构任务对象,因为会自动删除掉。
运行代码,查看效果:

可以看到,三个任务被同时丢进线程池,因为时间太过相近,所以实际上打印消息的顺序并不确定,但总体上是同时执行的,符合预期。
注意最大线程数,这个可以控制同时执行任务的数量。
我们修改成2,看会发生什么:
pool->setMaxThreadCount(2);

虽然打印信息有些杂乱,但可以看到,一开始执行的时候只有任务1和2,任务3直接排队去了。等前两个任务其中一个执行完毕,并析构销毁后,任务3才开始启动。
同时丢进线程池中的任务数量太多,比如几百上千,这种高度使用多线程已经将系统资源压榨到极限,出现了内存暴涨或CPU占用过高的情况,我们就可以手动设置最大线程数,来控制同时执行任务的线程数量。
另外,如果不希望它自动析构的话,可以设置以下代码,并手动析构:
setAutoDelete(false);
需要注意一点,QRunnable看上去和QThread差不多,但QRunnable并不是继承自QObject,也就是说它并不能直接通过信号槽的方式和外部进行通信。
这个时候,有两种方案。
第一种,多重继承,也就是:
class MyTask : public QObject, public QRunnable
{
Q_OBJECT
这种情况下,第一个继承的必须是QObject,且需要将自动删除禁用掉,自己来管理删除。
第二种,使用接受者回调:
class LightTask : public QRunnable
{
public:
LightTask(MainWindow* receiver, int id)
: m_receiver(receiver), m_id(id) {}
void run() override {
// 工作...
QThread::sleep(1);
// 安全地通知主线程
QMetaObject::invokeMethod(
m_receiver, "onTaskProgress",
Qt::QueuedConnection,
Q_ARG(int, m_id),
Q_ARG(int, 100)
);
}
private:
MainWindow* m_receiver; // 主线程对象
int m_id;
};
二、总结
有关线程池,我就不再过多介绍了。还是要实际使用后,才能领悟和解决许多问题。
最后作为总结,对比一下四种线程的使用方式,列个表格吧:



如何选择?
你想做“一次性计算”?(如:转图片、加法、解析文件)
→ 用 QtConcurrent(最简单、最安全)
你需要一个“长期运行的服务”?(如:监听 TCP、读取传感器、播放音频)
→ 用 QThread + moveToThread
你需要完全控制任务行为、优先级、或实现复杂调度?
→ 用 QThreadPool + QRunnable
你在写新代码?
→ 永远不要直接继承 QThread(除非你知道自己在做什么)
补充说明:
QtConcurrent 底层就是 QThreadPool,所以它本质是后者的高层封装。
moveToThread 是 Qt 官方推荐的“正确”使用 QThread 的方式。
所有后台线程中禁止直接访问 GUI 对象,必须通过信号、invokeMethod 或 QFutureWatcher 回到主线程。
最后:
希望我们都能更好地使用多线程!再也不会被这个东西所考倒咯!


3万+

被折叠的 条评论
为什么被折叠?



