目录
QThread%E7%B1%BB-toc" name="tableOfContents" style="margin-left:0px">一、继承QThread类
1)自定义线程类
2)使用自定义的子线程类
3)使用说明
QThread%E7%9A%84%E6%B6%88%E6%81%AF%E5%BE%AA%E7%8E%AF-toc" name="tableOfContents" style="margin-left:0px">二、利用QThread的消息循环
1)自定义执行类
2)启动子线程
3)方法说明
三、使用线程池
四、方法比较
QT多线程编程常见的有3种实现方法,一种是继承QThread类,一种是利用QThread的消息循环,还有一种是使用线程池。这两种方式在不同的场景下各有优势,下面对两种实现方法进行详细说明。
QThread%E7%B1%BB" name="%E4%B8%80%E3%80%81%E7%BB%A7%E6%89%BFQThread%E7%B1%BB">一、继承QThread类
直接继承QTread类是进行多线程编程最常用的一种方式。我们只需要定义一个自己的类,并继承自QThread即可。具体实现如下所示。
1)自定义线程类
首先我们在头文件中自定义一个类MyThread,在类中定义好构造函数、析构函数和run函数。run函数必须要重写,用于实现子线程需要完成的工作。当子线程启动时,将直接执行run函数中的代码,也只有run函数中的代码是在子线程中执行的。在我们自定义的MyThread类中,我们同样可以定义公共函数,比如setPara(),用于在启动子线程之前,为子线程对象设置参数信息。同时也可以定义信号,用于子线程向外传递参数。在类MyThread的对象中,虽然run函数中的代码运行在子线程中,但run函数依然可以访问对象中的各类成员。但当run函数访问对象中的公有成员时,若创建该子线程对象的线程也需要访问该公有成员,则可能会存在访问冲突的问题,这需要在具体实现时加以注意,避免多个线程同时访问同一内存。
类MyThread的CPP文件实现不再赘述,与所有类的实现一样。
mythread.h头文件#ifndef MYTHREAD_H
#define MYTHREAD_H#include <QThread>
#include <QObject>class MyThread : public QThread
{Q_OBJECT
public:MyThread (QObject *parent = nullptr);~MyThread ();
protected:void run();//重写该函数,实现子线程需要完成的工作
private:int a,b,c;
public:void setPara(int,int,int);
signals:void sendValue(int);
};
#endif // MYTHREAD_H
2)使用自定义的子线程类
使用继承自QThread的类也非常简单。首先在我们需要使用的地方,包含自定义类的头文件mythread.h,然后使用该类实例化一个对象,在启动线程前,可以调用该对象的公有函数设置一些参数,最后调用对象的start()函数启动线程。如果需要从子线程中传递出参数,则可以使用信号和槽将子线程对象的信号与当前的槽函数连接。
3)使用说明
需要指出的是,调用对象的start()函数后,将默认自动执行run函数中的代码。run函数执行完成后,线程将自动退出,子线程中不会运行消息循环机制。你可以多次调用start()函数,前提是线程已经成功退出,否则run函数不会执行。因此,需要重启子线程前,你需要判断前一次启动的线程是否真的已经成功退出(if (!mythread.isRunning()) { thread.start(); } else { thread.wait(); })。你确实需要这么做,因为真正结束子线程是操作系统来控制的,run函数执行完成只代表它的工作已经完成,但子线程的资源未必立即释放。
在下面的例子中,我们直接在main函数中定义了子线程类,并启动了子线程,也就是我们直接在程序的主线程中创建了一个子线程。那么这个子线程对象的所有资源其实都属于主线程,我们可以在主线程中通过子线程对象myThread访问到该对象的所有公有成员。对象myThread不属于子线程,属于子线程的资源是在run函数中定义的对象。
#include "mythread.h"
#include <QApplication>
int main(int argc, char *argv[])
{QApplication a(argc, argv);MyThread myThread;myThread.setPra(1,2,3);myThread.start();return a.exec();
}
QThread%E7%9A%84%E6%B6%88%E6%81%AF%E5%BE%AA%E7%8E%AF" name="%E4%BA%8C%E3%80%81%E5%88%A9%E7%94%A8QThread%E7%9A%84%E6%B6%88%E6%81%AF%E5%BE%AA%E7%8E%AF">二、利用QThread的消息循环
有时候我们可能不能确定何时需要启动子线程,而是要根据任务需要由程序类决定是否需要使用子线程。在这种情况,我们可以利用QThread的消息循环机制来实现,具体如下是QT帮助文档提供的示例,下面对示例代码进行逐一解释。
1)自定义执行类
首先我们定义了一个需要在子线程中执行的类,该类完全由用户自定义,继承自基类QObject。然后在类中实现我们需要执行的业务操作函数,该函数必须定义为共有的槽函数,因为它需要由外部连接触发。我们也同样可以定义信号,向外部传递信息。
2)启动子线程
在需要使用子线程的地方,首先创建一个QThread对象workerThread,这就是一个基本的QThread对象,然后再创建一个前面自定义的执行类对象worker,随后调用对象worker的moveToThread()函数,将worker移入到workerThread中,再连接信号和槽,最后调用workerThread的start函数启动子线程。
class Worker : public QObject//定义一个需要在子线程中运行的类{Q_OBJECTpublic slots:void doWork(const QString ¶meter) {//在子线程中需要执行的函数QString result;emit resultReady(result);}signals:void resultReady(const QString &result);//对外发送的信号};//以下是如何使用子线程class Controller : public QObject{Q_OBJECTQThread workerThread;//创建一个QThread 对象public:Controller() {Worker *worker = new Worker;//创建一个需要在子线程中运行的对象worker->moveToThread(&workerThread);//将子线程中运行的对象移入到workerThread对象中connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);//线程结束后删除对象connect(this, &Controller::operate, worker, &Worker::doWork);//链接外部信号,执行子线程的工作connect(worker, &Worker::resultReady, this, &Controller::handleResults);workerThread.start();//启动子线程,默认启动消息循环机制}~Controller() {workerThread.quit();//退出子线程workerThread.wait();}public slots:void handleResults(const QString &);signals:void operate(const QString &);};
3)方法说明
从前面的示例代码中我们可以发现,执行类Worker就是一个普通的类,它没有任何与线程相关的特殊定义或设置。moveToThread()函数是关键,它是QObject基类的成员函数,因此所有继承他的类的对象都可以调用该函数。示例代码中3个信号和槽连接,前两个是必要的,最后一个根据需要可以使用。当然,如果执行类Worker中有多个槽函数实现不同的业务工作,我们可以在添加连接更多的信号和槽。
子线程的启动我们直接调用了start()函数,这里的启动机制与前述有所不同,它将直接创建子线程,并在子线程中运行消息循环机制,而不是运行run函数,这里我们也没有实现run函数。子线程将一直存在,直至我们调用quit()函数,或者主程序结束。即使在主程序退出前,也建议调用quit()函数,如上述示例代码所示,以确保成功释放资源。
由于子线程一直存在,并运行着自己独立的消息循环,当上述代码中第二个连接的信号触发后,Worker类的dowork()槽函数就会执行,而它就是在子线程中执行的,并不是在创建Worker类对象的线程中(如主线程)。需要指出的是,虽然Worker类的dowork()槽函数在子线程中执行,但是Worker类的对象worker资源同样不属于子线程,而是创建它的线程。子线程只负责运行代码,访问资源,但并不管理对象worker的资源。
三、使用线程池
首先看一下官方介绍:
QThreadPool管理和重新设置单个QThread对象,以帮助降低使用线程的程序中的线程创建成本。每个Qt应用程序都有一个全局QThreadPool对象,可以通过调用globalInstance()访问该对象。要使用QThreadPool线程,请子类QRunnable并实现run()虚拟函数。然后创建该类的一个对象并将其传递给QThreadPool::start()。
QT提供的线程池,使QT的线程使用更加方便灵活。官方示例代码如下,我们只需要子类化QRunnable,定义执行类,然后重写run函数,再将执行类的对象传递给QThreadPool::globalInstance()->start(hello)即可。
这里使用的是Qt应用程序的全局QThreadPool对象,我们当然也可以定义一个自己的QThreadPool *threadPool对象,便于对线程池一些参数的设置。比如我们可以设置线程池中最大的线程数:threadPool->setMaxThreadCount(20)。
class HelloWorldTask : public QRunnable{void run() override{qDebug() << "Hello world from thread" << QThread::currentThread();}};HelloWorldTask *hello = new HelloWorldTask();QThreadPool::globalInstance()->start(hello);
四、方法比较
QT提供的3中多线程实现方法可根据需要选择。对于第一种直接继承QThread类, 它可以适用于需要长时间持续存在的,或者有阻塞机制的业务中。比如我们要用QT实现一个网络数据接收套接字程序,我们需要持续监听端口,子线程需要阻塞,这时就可以使用这种方式。对于第二种QThread消息循环机制,在我们需要频繁执行相同业务,但又不能事先确定何时调用时可以使用,比如处理较大的网络数据包时,可能因为网络数据包很大,处理时间较长,我们不愿交给主线程去做,这时就可以通过这种方式让子线程来处理。当收到数据包后,利用信号和槽机制,调用子线程中的参函数处理数据。第三种线程池的方式则更加灵活,通常在处理大批量任务时比较适用,比如在并行计算中。总体而言,任何子线程业务工作都可以通过上述三种方式来实现,其区别主要是程序的开发便捷度和资源的优化利用上。
最后需要指出的是,在子线程中我们应慎重使用无限循环,否则必须要为子线程的退出设置终止条件。