Qt线程使用(六)QThreadPool + QRunnable

前言

本节将介绍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 回到主线程。
最后:
希望我们都能更好地使用多线程!再也不会被这个东西所考倒咯!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值