目录
Qt最简单的多线程方法QtConcurrent::run() (***)
Qt 的4种多线程实现详解 (** 测试代码)
QtConcurrent 线程使用详解
Qt多线程的三种方法QThread(**总结:能做与不能做)
【QT】 Qt多线程的“那些事”
-------------------------------------------------
============================
Qt多线程的三种方法QThread
三、QThread总结
推荐做的:
在QThread子类添加信号。这是绝对安全的,并且也是正确的(发送者的线程依附性没有关系)
不应该做的是:
调用moveToThread(this)函数
指定连接类型:这通常意味着你正在做错误的事情,比如将QThread控制接口与业务逻辑混杂在了一起(而这应该放在该线程的一个独立对象中)
在QThread子类添加槽函数:这意味着它们将在错误的线程被调用,也就是QThread对象所在线程,而不是QThread对象管理的线程。这又需要你指定连接类型或者调用moveToThread(this)函数
使用QThread::terminate()函数
不能做的是:
在线程还在运行时退出程序。使用QThread::wait()函数等待线程结束
在QThread对象所管理的线程仍在运行时就销毁该对象。如果你需要某种“自行销毁”的操作,你可以把 finished()信号同 deleteLater()槽连接起来
=====================================
Qt最简单的多线程方法QtConcurrent::run()
最近编写了一个软件,没有考虑多线程的问题,编好以后,软件在执行计算的时候,鼠标响应有时候会延迟,但是完全能用,考虑到后续随着计算任务的增加,鼠标响应可能延迟会更大,所以打算使用多线程的方法,将执行计算的任务丢到另一个线程进行处理,不影响主界面对鼠标以及各个控件的响应。
查了一下书以及上网搜了一下,介绍的最多的就是子类化QThread,然后重载run(),这种操作可以实现多线程,但是我的软件基本已经成形,如果再通过重载run()实现,软件改动会很大,
然后我就上网查有没有什么更简单的方法,使自己的软件代码改动最小,最后查到可以使用QtConcurrent::run()的方法。根据网上说的,QtConcurrent::run()这个方法好像是较新版本Qt才支持,所以现在网上搜索Qt多线程的操作搜索到这个方法的不是很普遍。自己按照搜索结果,编写程序进行验证,发现确实很方便,对原软件架构改动非常小。
新建一个工程,创建ui界面。
首先需要在工程文件.pro中添加下面一句:
QT += concurrent ///步骤(1)
在ui界面添加三个按钮,如下图所示:
开始按钮就是不使用多线程执行一个死循环(用这个死循环代替系统原有函数功能),点击开始以后,整个软件陷入死循环,无法响应任何操作,多线程启动就是通过多线程方法执行相同的死循环,停止按钮就是退出这个死循环。
main.c代码如下:
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
mainwindow.h代码如下:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
void star();//多线程启动函数,在原有的代码基础上增加该函数
void xunhuan();//死循环函数,用这个函数代替原有代码的函数
~MainWindow();
private slots:
void on_pushButton_clicked();
void on_pushButton_2_clicked();
void on_pushButton_3_clicked();
private:
Ui::MainWindow *ui;
int myEnable;//标志位,用于退出死循环操作
};
#endif // MAINWINDOW_H
mainwindow.cpp的代码如下:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include<QDebug>
#include<QtConcurrent>//要记得添加该头文件
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
myEnable = 0;//死循环标志位
}
void MainWindow::xunhuan() //死循环操作,代替原有代码的函数功能
{
int i=0;
while(myEnable)
{
i++;
qDebug()<<i;
}
}
///步骤(2)
void MainWindow::star() //启动函数是需要在原有代码基础上增加
{///步骤(3)
QtConcurrent::run(this,&MainWindow::xunhuan); //多线程执行死循环启动,可以带参数,具体格式可以查阅网上其它资料
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_pushButton_clicked()
{
myEnable = 1;
xunhuan(); //非多线程执行死循环
}
void MainWindow::on_pushButton_2_clicked()
{
myEnable=0;//标志位置零,退出死循环
qDebug()<<"退出死循环!";
}
void MainWindow::on_pushButton_3_clicked()
{
myEnable = 1;///步骤(4)
star(); //多线程启动死循环
}
测试结果,使用非多线程方法启动死循环,整个程序陷入死循环,主界面无法响应任何操作。
使用多线程方法启动死循环,程序还可以正常响应操作,当点击停止按钮以后,能够退出死循环,结果如下:
自己只是了解皮毛,更深层次的用法还没仔细研究。
————————————————
版权声明:本文为CSDN博主「guo88455648」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/guo88455648/article/details/82736202
=====================================
【QT】 Qt多线程的“那些事”
作者:李春港
出处:https://www.cnblogs.com/lcgbk/p/13967448.html
目录
- 一、前言
- 二、QThread源码浅析
- 三、四种Qt多线程的实现方法
- 四、跨线程的信号槽
- 五、总结
一、前言#
在我们开发Qt程序时,会经常用到多线程和信号槽的机制,将耗时的事务放到单独的线程,将其与GUI线程独立开,然后通过信号槽的机制来进行数据通信,避免GUI界面假死的情况。
例如:
使用QT实现文件的传送,并且GUI界面需要实时显示发送的进度,这时就需要将耗时的文件数据操作放到独立的线程中,然后把已传送的进度数据通过信号发送到GUI线程,GUI主线程接收到信号后通过槽函数来更新UI,这样界面就不会出现假死的情况了。
多线程和信号槽机制都是QT的关键技术之一。理解清楚这两个技术点的关系,会让你在开发过程中少走些弯路,少踩一些坑。
本文章会介绍多种Qt多线程的实现方法,但是主要还是介绍有关于 信号槽机制的多线程 实现方法。在学习QT多线程的"那些事"前,我们不妨先思考下以下的一些问题,然后再带着问题继续往下看,这样可能会有更好的理解:
【1】如何正确使用QT的多线程?
【2】线程start后,哪里才是线程正在启动的地方?
【3】如何正确结束子线程以及资源释放?
【4】重复调用QThread::start、QThread::quit()或QThread::exit()、QThread::terminate函数会有什么影响?
【5】调用QThread::quit()或QThread::exit()、QThread::terminate函数会不会立刻停止线程?
【6】多线程之间是怎么进行通信的?
【7】如何在子线程中启动信号与槽的机制?
【8】QT中多线程之间的信号和槽是如何发送或执行的?
【9】如何正确使用信号与槽机制?
接下来我会通过我以前踩过的坑和开发经验,并且通过一些实例来总结一下QT多线程和QT信号槽机制的知识点。
这个是本文章实例的源码地址:https://gitee.com/CogenCG/QThreadExample.git
二、QThread源码浅析#
本章会挑出QThread源码中部分重点代码来说明QThread启动到结束的过程是怎么调度的。其次因为到了Qt4.4版本,Qt的多线程就有所变化,所以本章会以Qt4.0.1和Qt5.6.2版本的源码来进行浅析。
2.1 QThread类的定义源码#
Qt4.0.1版本源码:
#ifndef QT_NO_THREAD
class Q_CORE_EXPORT QThread : public QObject
{
public:
...//省略
explicit QThread(QObject *parent = 0);
~QThread();
...//省略
void exit(int retcode = 0);
public slots:
void start(QThread::Priority = InheritPriority); //启动线程函数
void terminate(); //强制退出线程函数
void quit(); //线程退出函数
...//省略
signals:
void started(); //线程启动信号
void finished(); //线程结束信号
...//省略
protected:
virtual void run() = 0;
int exec();
...//省略
};
#else // QT_NO_THREAD
Qt5.6.2版本源码:
#ifndef QT_NO_THREAD
class Q_CORE_EXPORT QThread : public QObject
{
Q_OBJECT
public:
...//省略
explicit QThread(QObject *parent = Q_NULLPTR);
~QThread();
...//省略
void exit(int retcode = 0); //线程退出函数
...//省略
public Q_SLOTS:
void start(Priority = InheritPriority); //启动线程函数
void terminate(); //强制退出线程函数
void quit(); //线程退出函数
...//省略
Q_SIGNALS:
void started(QPrivateSignal); //线程启动信号
void finished(QPrivateSignal); //线程结束信号
protected:
virtual void run();
int exec();
...//省略
};
#else // QT_NO_THREAD
从以上两个版本的代码可以看出,这些函数在声明上基本没什么差异,但是仔细看,两个版本的 run() 函数声明的是不是不一样?
- Qt4.0.1版本run() 函数是纯虚函数,即此类为抽象类不可以创建实例,只可以创建指向该类的指针,也就是说如果你需要使用QThread来实现多线程,就必须实现QThread的派生类并且实现 run() 函数;
- Qt5.6.2版本的run() 函数是虚函数,继承QThread类时,可以重新实现 run() 函数,也可以不实现。
注:我查看了多个Qt版本的源码,发现出现以上差异的版本是从Qt4.4开始的。从Qt4.4版本开始,QThread类就不再是抽象类了。
2.2 QThread::start()源码#
再来看看QThread::start()源码,Qt4.0.1版本和Qt5.6.2版本此部分的源码大同小异,所以以Qt5.6.2版本的源码为主,如下:
void QThread::start(Priority priority)
{
Q_D(QThread);
QMutexLocker locker(&d->mutex);
if (d->isInFinish) {
locker.unlock();
wait();
locker.relock();
}
if (d->running)
return;
... ... // 此部分是d指针配置
#ifndef Q_OS_WINRT
... ... // 此部分为注释
d->handle = (Qt::HANDLE) _beginthreadex(NULL, d->stackSize, QThreadPrivate::start,
this, CREATE_SUSPENDED, &(d->id));
#else // !Q_OS_WINRT
d->handle = (Qt::HANDLE) CreateThread(NULL, d->stackSize, (LPTHREAD_START_ROUTINE)QThreadPrivate::start,
this, CREATE_SUSPENDED, reinterpret_cast<LPDWORD>(&d->id));
#endif // Q_OS_WINRT
if (!d->handle) {
qErrnoWarning(errno, "QThread::start: Failed to create thread");
d->running = false;
d->finished = true;
return;
}
int prio;
d->priority = priority;
switch (d->priority) {
... ... // 此部分为线程优先级配置
case InheritPriority:
default:
prio = GetThreadPriority(GetCurrentThread());
break;
}
if (!SetThreadPriority(d->handle, prio)) {
qErrnoWarning("QThread::start: Failed to set thread priority");
}
if (ResumeThread(d->handle) == (DWORD) -1) {
qErrnoWarning("QThread::start: Failed to resume new thread");
}
}
挑出里面的重点来说明:
(1)Q_D() 宏定义
在看源码的时候,当时比较好奇start函数的第一条语句 Q_D()宏定义 是什么意思,所以就看了下源码,在此也顺便讲讲,Q_D() 源码是一个宏定义,如下:
#define Q_D(Class) Class##Private * const d = d_func()
此处利用了预处理宏里的 ## 操作符:连接前后两个符号,变成一个新的符号。将Q_D(QThread)展开后,变成:QThreadPrivate * const d = d_func()。
(2)_beginthreadex()函数
上面d->handle = (Qt::HANDLE) _beginthreadex ( NULL, d->stackSize, QThreadPrivate::start, this, CREATE_SUSPENDED, &( d->id ) ) 语句中的函数是创建线程的函数,其原型以及各参数的说明如下:
unsigned long _beginthreadex(
void *security, // 安全属性,NULL为默认安全属性
unsigned stack_size, // 指定线程堆栈的大小。如果为0,则线程堆栈大小和创建它的线程的相同。一般用0
unsigned ( __stdcall *start_address )( void * ),
// 指定线程函数的地址,也就是线程调用执行的函数地址(用函数名称即可,函数名称就表示地址)
void *arglist, // 传递给线程的参数的指针,可以通过传入对象的指针,在线程函数中再转化为对应类的指针
//如果传入this,这个this表示调用QThread::start的对象地址,也就是QThread或者其派生类对象本身
unsigned initflag, // 线程初始状态,0:立即运行;CREATE_SUSPEND:suspended(悬挂)
unsigned *thrdaddr // 用于记录线程ID的地址
);
2.3 QThreadPrivate::start()源码#
从QThread::start()源码可以知道,QThreadPrivate::start是重点,其实际就是调用了QThreadPrivate::start(this),这个 this 表示调用QThread::start的对象地址,也就是QThread或者其派生类对象本身。因为两个Qt版本此部分的源码大同小异,所以本部分主要是以5.6.2版本的源码为主,其源码以及说明如下:
// 参数arg就是上面所说的this
unsigned int __stdcall QT_ENSURE_STACK_ALIGNED_FOR_SSE QThreadPrivate::start(void *arg)
{
QThread *thr = reinterpret_cast<QThread *>(arg);
QThreadData *data = QThreadData::get2(thr);
// 创建线程局部存储变量,存放线程id
qt_create_tls();
TlsSetValue(qt_current_thread_data_tls_index, data);
data->threadId = reinterpret_cast<Qt::HANDLE>(quintptr(GetCurrentThreadId()));
QThread::setTerminationEnabled(false);
{
QMutexLocker locker(&thr->d_func()->mutex);
data->quitNow = thr->d_func()->exited;
}
if (data->eventDispatcher.load()) // custom event dispatcher set?
data->eventDispatcher.load()->startingUp();
else
createEventDispatcher(data);
...//省略
emit thr->started(QThread::QPrivateSignal()); // 发射线程启动信号
QThread::setTerminationEnabled(true);
thr->run(); // 调用QThread::run()函数 -- 线程函数
finish(arg); //结束线程
return 0;
}
由上述源码可以看出,实际上 run() 函数是在这里调用的,并且发出了 started() 启动信号,等到 run() 函数执行完毕,最后是调用了 QThreadPrivate::finish 函数结束线程,并且在finish内会发出 QThread::finished() 线程已结束的信号。
2.4 QThread::run()源码#
再看看QThread::run()函数的源码。在上面 《2.1 QThread类的定义源码》的小节,我们可以看到两个Qt版本声明此方法的方式不一样,Qt-4.0版本将此定义为了纯虚函数,而Qt-5.6版本将此定义为了虚函数,那我们就看看Qt-5.6版本中,QThread::run()是如何定义的,如下:
void QThread::run()
{
(void) exec();
}
-
每一个 Qt 应用程序至少有一个 事件循环 ,就是调用了 QCoreApplication::exec() 的那个事件循环。不过,QThread也可以开启事件循环。只不过这是一个受限于线程内部的事件循环。因此我们将处于调用main()函数的那个线程,并且由 QCoreApplication::exec() 创建开启的那个事件循环成为 主事件循环 ,或者直接叫 主循环 。注意,QCoreApplication::exec()只能在调用main()函数的线程调用。主循环所在的线程就是主线程,也被成为 GUI 线程,因为所有有关 GUI 的操作都必须在这个线程进行。QThread的局部事件循环则可以通过在 QThread::run() 中调用 QThread::exec() 开启。
-
我们通过以上源码可以看到,它的定义很简单,就是调用了一个函数:QThread::exec() 开启线程中的 事件循环 ,我们也可以通过继承QThread,重写run()函数的方式,让其实现相对复杂的逻辑代码。如果你的线程需要将某些槽函数在本线程完成的话,就必须开启事件循环,否则在线程内无法响应各种信号并作出相应的行为。
小结:
比Qt-4.4版本更早的版本中,我们使用QThread启动线程时,就必须要实现继承于QThread的派生类,并且一定要重写run函数,若需要使用事件循环,就需要在run函数中添加exec()。
到了Qt4.4版本之后(包括Qt4.4版本),QThread就不是抽象类了,不派生也可以实例化,在不重写QThread::run()方法,start启动线程是默认启动事件循环的。
注:当程序跑到了exec()代码时,位于exec()后面的代码就不会再被执行,除非我们使用quit、exit等退出语句来退出事件循环,退出后,程序才会继续执行位于exec()后面的代码。
2.5 QThread::quit()、QThread::exit()、QThread::terminate()源码#
线程停止函数的区别,从Qt源码来分析:
(1)QThread::quit()、QThread::exit()
//QThread::quit()声明
void quit();
//QThread::quit()定义
void QThread::quit()
{ exit(); }
//QThread::exit()声明
void exit(int retcode = 0);
//QThread::exit()定义
void QThread::exit(int returnCode)
{
Q_D(QThread);
QMutexLocker locker(&d->mutex);
d->exited = true;
d->returnCode = returnCode;
d->data->quitNow = true;
for (int i = 0; i < d->data->eventLoops.size(); ++i) {
QEventLoop *eventLoop = d->data->eventLoops.at(i);
eventLoop->exit(returnCode);
}
}
由以上源码可知,QThread::quit() 和 QThread::exit(0) 的调用是等效的,都是告诉线程的事件循环,以返回码0(成功)退出。如果线程没有事件,则此函数不执行任何操作,也就是无效的。当线程拥有事件循环并且正处于 事件循环(QThread::exec()) 的状态时,调用 QThread::quit()或者QThread::exit() 线程就会马上停止,否则不会立刻停止线程,直到线程处于事件循环也就是正在执行 QThread::exec() 时,才会停止线程。
如果重复调用 QThread::quit()或者QThread::exit() 会有什么影响吗?
重复调用 QThread::quit()或者QThread::exit() 也不会有什么影响,因为只有拥有事件循环的线程,这两个函数才会生效停止线程的功能。
(2)QThread::terminate()
void QThread::terminate()
{
Q_D(QThread);
QMutexLocker locker(&d->mutex);
if (!d->running)
return;
if (!d->terminationEnabled) {
d->terminatePending = true;
return;
}
// Calling ExitThread() in setTerminationEnabled is all we can do on WinRT
#ifndef Q_OS_WINRT
TerminateThread(d->handle, 0);
#endif
QThreadPrivate::finish(this, false); //结束线程函数
}
在这个函数定义的最后一个语句,是调用了 QThreadPrivate::finish(this, false); 函数,其函数作用是直接退出线程,无论线程是否开启了事件循环都会生效,会马上终止一个线程,但这个函数存在非常不安定因素,不推荐使用。
如果重复调用 QThread::terminate() 会有什么影响吗?
没有影响。我们可以看到函数体里面的第三条语句,它首先会判断线程是否还在运行中,如果不是,会直接退出函数,就不会继续往下执行调用QThreadPrivate::finish(this, false); 函数了。
2.6 章节小结#
相信看了以上的一些QThread源码,都大概知道了QThread类的本质以及QThread开启到结束的过程。这里我再简单总结下:
(1)QThread的本质:
- QThread 是用来管理线程的,它所依附的线程和它管理的线程并不是同一个东西;
- QThread 所依附的线程,就是执行 QThread t 或 QThread * t=new QThread 所在的线程;
- QThread 管理的线程,就是 run 启动的线程,也就是次线程。
(2)在这里针对Qt4.4版本之后(包括Qt4.4版本)简单汇总一下线程启动到结束的过程:
- QThread对象或者QThread派生类对象显式调用QThread类中的外部start()方法;
- QThread::start()方法再调用QThreadPrivate::start()方法;
- 在QThreadPrivate::start()方法内调用了QThread::run()虚函数,对使用者来说到了这里才是真正进入了一个新的线程里面。也就是说定义QThread对象或者QThread派生类对象的时候,还是在原来的线程里面,只有进入run函数才是进入了新的线程;
- 在QThreadPrivate::start()方法调用QThread::run()虚函数结束后,就会继续调用QThreadPrivate::finish()函数来结束线程,并发出线程结束的信号finished()。
(3)QThread::quit()、QThread::exit()、QThread::terminate():
- 对线程重复使用这三个停止线程的函数,没有任何影响;
- 尽量不要使用QThread::terminate()停止线程,此方式是强制退出线程,没有安全保障。
- 调用QThread::quit()和QThread::exit()一样。
(4)Qt各版本QThread类的变化:
- Qt4.4版本之前QThread类是属于抽象类, Qt4.4版本之后(包括4.4版本)不是抽象类。
三、四种Qt多线程的实现方法#
Qt的多线程实现方法主要有四种形式:子类化QThread、子类化QObject+moveToThread、继承QRunnable+QThreadPool、QtConcurrent::run()+QThreadPool。本文章会注重介绍前两种实现方法:子类化QThread、子类化QObject+moveToThread,也会简单介绍后两种的使用。
注:QtConcurrent、QRunnable以及QThreadPool的类,在Qt-4.4版本才开始有。
3.1 子类化QThread#
子类化QThread来实现多线程, QThread只有run函数是在新线程里的,其他所有函数都在QThread生成的线程里。正确启动线程的方法是调用QThread::start()来启动,如果直接调用run成员函数,这个时候并不会有新的线程产生( 原因: 可以查看第一章,run函数是怎么被调用的)
3.1.1 步骤#
- 子类化 QThread;
- 重写run,将耗时的事件放到此函数执行;
- 根据是否需要事件循环,若需要就在run函数中调用 QThread::exec() ,开启线程的事件循环。事件循环的作用可以跳到《2.4 QThread::run()源码》小节进行阅读;
- 为子类定义信号和槽,由于槽函数并不会在新开的线程运行,所以需要在构造函数中调用 moveToThread(this)。 注意:虽然调用moveToThread(this)可以改变对象的线程依附性关系,但是QThread的大多数成员方法是线程的控制接口,QThread类的设计本意是将线程的控制接口供给旧线程(创建QThread对象的线程)使用。所以不要使用moveToThread()将该接口移动到新创建的线程中,调用moveToThread(this)被视为不好的实现。
接下来会通过《使用线程来实现计时器,并实时在UI上显示》的实例来说明不使用事件循环和使用事件循环的情况。(此实例使用QTimer会更方便,此处为了说明QThread的使用,故使用线程来实现)
3.1.2 不使用事件循环实例#
InheritQThread.hpp
class InheritQThread:public QThread
{
Q_OBJECT
public:
InheritQThread(QObject *parent = Q_NULLPTR):QThread(parent){
}
void StopThread(){
QMutexLocker lock(&m_lock);
m_flag = false;
}
protected:
//线程执行函数
void run(){
qDebug()<<"child thread = "<<QThread::currentThreadId();
int i=0;
m_flag = true;
while(1)
{
++i;
emit ValueChanged(i); //发送信号不需要事件循环机制
QThread::sleep(1);
{
QMutexLocker lock(&m_lock);
if( !m_flag )
break;
}
}
}
signals:
void ValueChanged(int i);
public:
bool m_flag;
QMutex m_lock;
};
mainwindow.hpp
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr) :
QMainWindow(parent),
ui(new Ui::MainWindow){
ui->setupUi(this);
qDebug()<<"GUI thread = "<<QThread::currentThreadId();
WorkerTh = new InheritQThread(this);
connect(WorkerTh, &InheritQThread::ValueChanged, this, &MainWindow::setValue);
}
~MainWindow(){
delete ui;
}
public slots:
void setValue(int i){
ui->lcdNumber->display(i);
}
private slots:
void on_startBt_clicked(){
WorkerTh->start();
}
void on_stopBt_clicked(){
WorkerTh->StopThread();
}
void on_checkBt_clicked(){
if(WorkerTh->isRunning()){
ui->label->setText("Running");
}else{
ui->label->setText("Finished");
}
}
private:
Ui::MainWindow *ui;
InheritQThread *WorkerTh;
};
在使用多线程的时候,如果出现共享资源使用,需要注意资源抢夺的问题,例如上述InheritQThread类中m_flag变量就是一个多线程同时使用的资源,上面例子使用 QMutexLocker+QMutex 的方式对临界资源进行安全保护使用,其实际是使用了 RAII技术:(Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。具体 QMutexLocker+QMutex 互斥锁的原理以及使用方法,在这里就不展开说了,这个知识点网上有很多非常好的文章。
效果:
(1)在不点【start】按键的时候,点击【check thread state】按钮检查线程状态,该线程是未开启的。
(2)按下【start】后效果如下,并查看终端消息打印信息:
只有调用了QThread::start()后,子线程才是真正的启动,并且只有在run()函数才处于子线程内。
(3)我们再试一下点击【stop】按钮,然后检查线程的状态:
点击【stop】按钮使 m_flag = false, 此时run函数也就可以跳出死循环,并且停止了线程的运作,之后我们就不能再次使用该线程了,也许有的人说,我再一次start不就好了吗?再一次start已经不是你刚才使用的线程了,这是start的是一个全新的线程。到此子类化 QThread ,不使用事件循环的线程使用就实现了,就这么简单。
3.1.3 使用事件循环实例#
run函数中的 while 或者 for 循环执行完之后,如果还想让线程保持运作,后期继续使用,那应该怎么做?
可以启动子线程的事件循环,并且使用信号槽的方式继续使用子线程。注意:一定要使用信号槽的方式,否则函数依旧是在创建QThread对象的线程执行。
- 在run函数中添加QThread::exec()来启动事件循环。(注意: 在没退出事件循环时,QThread::exec()后面的语句都无法被执行,退出后程序会继续执行其后面的语句);
- 为QThread子类定义信号和槽;
- 在QThread子类构造函数中调用 moveToThread(this)(注意: 可以实现构造函数在子线程内执行,但此方法不推荐,更好的方法会在后面提到)。
接着上述的实例,在InheritQThread类构造函数中添加并且调用moveToThread(this);在run函数中添加exec();并定义槽函数:
/**************在InheritQThread构造函数添加moveToThread(this)**********/
InheritQThread(QObject *parent = Q_NULLPTR):QThread(parent){
moveToThread(this);
}
/**************在InheritQThread::run函数添加exec()***************/
void run(){
qDebug()<<"child thread = "<<QThread::currentThreadId();
int i=0;
m_flag = true;
while(1)
{
++i;
emit ValueChanged(i);
QThread::sleep(1);
{
QMutexLocker lock(&m_lock);
if( !m_flag )
break;
}
}
exec(); //开启事件循环
}
/************在InheritQThread类中添加QdebugSlot()槽函数***************/
public slots:
void QdebugSlot(){
qDebug()<<"QdebugSlot function is in thread:"<<QThread::currentThreadId();
}
在MainWindow类中添加QdebugSignal信号;在构造函数中将QdebugSignal信号与InheritQThread::QdebugSlot槽函数进行绑;添加一个发送QdebugSignal信号的按钮:
/**********在MainWindow构造函数中绑定信号槽******************/
explicit MainWindow(QWidget *parent = nullptr) :
QMainWindow(parent),
ui(new Ui::MainWindow){
qDebug()<<"GUI thread = "<<QThread::currentThreadId();
ui->setupUi(this);
WorkerTh = new InheritQThread(this);
connect(WorkerTh, &InheritQThread::ValueChanged, this, &MainWindow::setValue);
connect(this, &MainWindow::QdebugSignal, WorkerTh, &InheritQThread::QdebugSlot); //绑定信号槽
}
/********MainWindow类中添加信号QdebugSignal槽以及按钮事件槽函数**********/
signals:
void QdebugSignal(); //添加QdebugSignal信号
private slots:
//按钮的事件槽函数
void on_SendQdebugSignalBt_clicked()
{
emit QdebugSignal();
}
实现事件循环的程序已修改完成,来看下效果:
(1)在运行的时候为什么会出现以下警告?
QObject::moveToThread: Cannot move objects with a parent
我们看到MainWindow类中是这样定义InheritQThread类对象的:WorkerTh = new InheritQThread(this)。如果需要使用moveToThread()来改变对象的依附性,其创建时不能够带有父类。将语句改为:WorkerTh = new InheritQThread()即可。
(2)修改完成后,点击【start】启动线程,然后点击【stop】按钮跳出run函数中的while循环,最后点击【check thread state】按钮来检查线程的状态,会是什么样的情况呢?
由上图可以看到,线程依旧处于运行状态,这是因为run函数中调用了exec(),此时线程正处于事件循环中。
(3)接下来再点击【Send QdebugSignal】按钮来发送QdebugSignal信号。
由终端的打印信息得知,InheritQThread::QdebugSlot槽函数是在子线程中执行的。
3.1.4 子类化QThread线程的信号与槽#
从上图可知,事件循环是一个无止尽循环,事件循环结束之前,exec()函数后的语句无法得到执行。只有槽函数所在线程开启了事件循环,它才能在对应信号发射后被调用。无论事件循环是否开启,信号发送后会直接进入槽函数所依附的线程的事件队列,然而,只有开启了事件循环,对应的槽函数才会在线程中得到调用。下面通过几种情况来验证下:
(1)代码和《3.1.3 使用事件循环》小节的代码一样,然后进行如下的操作:点击【start】按钮->再点击【Send QdebugSignal】按钮,这个时候槽函数会不会被执行呢?
这种情况无论点多少次发送QdebugSignal信号,InheritQThread::QdebugSlot槽函数都不会执行。因为当前线程还处于while循环当中,如果需要实现槽函数在当前线程中执行,那么当前线程就应该处于事件循环的状态,也就是正在执行exec()函数。所以如果需要InheritQThread::QdebugSlot槽函数执行,就需要点击【stop】按钮退出while循环,让线程进入事件循环。
(2)在《3.1.3 使用事件循环》小节的代码基础上,把InheritQThread::run函数删除,然后进行如下的操作:点击【start】启动线程->点击【stop】按钮跳出run函数中的while循环进入事件循环->点击【Send QdebugSignal】按钮来发送QdebugSignal信号,会有什么结果呢?
结果会和上面第一种情况一样,虽然信号已经在子线程的事件队列上,但是由于子线程没有事件循环,所以槽函数永远都不会被执行。
(3)在上面《3.1.3 使用事件循环》小节的代码基础上,将InheritQThread构造函数中的 moveToThread(this) 去除掉。进行如下操作:点击【start】启动线程->点击【stop】按钮跳出run函数中的while循环进入事件循环->点击【Send QdebugSignal】按钮来发送QdebugSignal信号,会有什么结果呢?
由上图可以看出InheritQThread::QdebugSlot槽函数居然是在GUI主线程中执行了。因为InheritQThread对象我们是在主线程中new出来的,如果不使用moveToThread(this)来改变对象的依附性关系,那么InheritQThread对象就是属于GUI主线程,根据connect信号槽的执行规则,最终槽函数会在对象所依赖的线程中执行。信号与槽绑定的connect函数的细节会在后面的《跨线程的信号槽》章节进行单独介绍。
3.1.5 如何正确退出线程并释放资源#
InheritQThread类的代码不变动,和上述的代码一样:
#ifndef INHERITQTHREAD_H
#define INHERITQTHREAD_H
#include <QThread>
#include <QMutex>
#include <QMutexLocker>
#include <QDebug>
class InheritQThread:public QThread
{
Q_OBJECT
public:
InheritQThread(QObject *parent = Q_NULLPTR):QThread(parent){
moveToThread(this);
}
void StopThread(){
QMutexLocker lock(&m_lock);
m_flag = false;
}
protected:
//线程执行函数
void run(){
qDebug()<<"child thread = "<<QThread::currentThreadId();
int i=0;
m_flag = true;
while(1)
{
++i;
emit ValueChanged(i);
QThread::sleep(1);
{
QMutexLocker lock(&m_lock);
if( !m_flag )
break;
}
}
exec();
}
signals:
void ValueChanged(int i);
public slots:
void QdebugSlot(){
qDebug()<<"QdebugSlot function is in thread:"<<QThread::currentThreadId();
}
public:
bool m_flag;
QMutex m_lock;
};
#endif // INHERITQTHREAD_H
MainWindow类添加ExitBt、TerminateBt两个按钮,用于调用WorkerTh->exit(0)、WorkerTh->terminate()退出线程函数。由《2.5 QThread::quit()、QThread::exit()、QThread::terminate()源码》小节得知调用quit和exit是一样的,所以本处只添加了ExitBt按钮:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "ui_mainwindow.h"
#include "InheritQThread.h"
#include <QThread>
#include <QDebug>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr) :
QMainWindow(parent),
ui(new Ui::MainWindow){
qDebug()<<"GUI thread = "<<QThread::currentThreadId();
ui->setupUi(this);
WorkerTh = new InheritQThread();
connect(WorkerTh, &InheritQThread::ValueChanged, this, &MainWindow::setValue);
connect(this, &MainWindow::QdebugSignal, WorkerTh, &InheritQThread::QdebugSlot);
}
~MainWindow(){
delete ui;
}
signals:
void QdebugSignal();
public slots:
void setValue(int i){
ui->lcdNumber->display(i);
}
private slots:
void on_startBt_clicked(){
WorkerTh->start();
}
void on_stopBt_clicked(){
WorkerTh->StopThread();
}
void on_checkBt_clicked(){
if(WorkerTh->isRunning()){
ui->label->setText("Running");
}else{
ui->label->setText("Finished");
}
}
void on_SendQdebugSignalBt_clicked(){
emit QdebugSignal();
}
void on_ExitBt_clicked(){
WorkerTh->exit(0);
}
void on_TerminateBt_clicked(){
WorkerTh->terminate();
}
private:
Ui::MainWindow *ui;
InheritQThread *WorkerTh;
};
#endif // MAINWINDOW_H
运行上述的例程,点击【start】启动线程按钮,然后直接点击【exit(0)】或者【terminate()】,这样会直接退出线程吗?
点击【exit(0)】按钮(猛点)
点击【terminate()】按钮(就点一点)
由上述情况我们可以看到上面例程的线程启动之后,无论怎么点击【exit(0)】按钮,线程都不会退出,点击【terminate()】按钮的时候就会立刻退出当前线程。由《2.5 QThread::quit()、QThread::exit()、QThread::terminate()源码》小节可以得知,若使用QThread::quit()、QThread::exit()来退出线程,该线程就必须要在事件循环的状态(也就是正在执行exec()),线程才会退出。而QThread::terminate()不管线程处于哪种状态都会强制退出线程,但这个函数存在非常多不安定因素,不推荐使用。我们下面来看看如何正确退出线程。
(1)如何正确退出线程?
- 如果线程内没有事件循环,那么只需要用一个标志变量来跳出run函数的while循环,这就可以正常退出线程了。
- 如果线程内有事件循环,那么就需要调用QThread::quit()或者QThread::exit()来结束事件循环。像刚刚举的例程,不仅有while循环,循环后面又有exec(),那么这种情况就需要先让线程跳出while循环,然后再调用QThread::quit()或者QThread::exit()来结束事件循环。如下:
注意:尽量不要使用QThread::terminate()来结束线程,这个函数存在非常多不安定因素。
(2)如何正确释放线程资源?
退出线程不代表线程的资源就释放了,退出线程只是把线程停止了而已,那么QThread类或者QThread派生类的资源应该如何释放呢?直接 delete QThread类或者派生类的指针吗?当然不能这样做,千万别手动delete线程指针,手动delete会发生不可预料的意外。理论上所有QObject都不应该手动delete,如果没有多线程,手动delete可能不会发生问题,但是多线程情况下delete非常容易出问题,那是因为有可能你要删除的这个对象在Qt的事件循环里还排队,但你却已经在外面删除了它,这样程序会发生崩溃。 线程资源释放分为两种情况,一种是在创建QThread派生类时,添加了父对象,例如在MainWindow类中WorkerTh = new InheritQThread(this)让主窗体作为InheritQThread对象的父类;另一种是不设置任何父类,例如在MainWindow类中WorkerTh = new InheritQThread()。
- 1、创建QThread派生类,有设置父类的情况:
这种情况,QThread派生类的资源都让父类接管了,当父对象被销毁时,QThread派生类对象也会被父类delete掉,我们无需显示delete销毁资源。但是子线程还没结束完,主线程就destroy掉了(WorkerTh的父类是主线程窗口,主线程窗口如果没等子线程结束就destroy的话,会顺手把WorkerTh也delete这时就会奔溃了)。 注意:这种情况不能使用moveToThread(this)改变对象的依附性。 因此我们应该把上面MainWindow类的构造函数改为如下:
~MainWindow(){
WorkerTh->StopThread();//先让线程退出while循环
WorkerTh->exit();//退出线程事件循环
WorkerTh->wait();//挂起当前线程,等待WorkerTh子线程结束
delete ui;
}
- 2、创建QThread派生类,没有设置父类的情况:
也就是没有任何父类接管资源了,又不能直接delete QThread派生类对象的指针,但是QObject类中有 void QObject::deleteLater () [slot] 这个槽,这个槽非常有用,后面会经常用到它用于安全的线程资源销毁。我们通过以上的《2.3 QThreadPrivate::start()源码》小节可知线程结束之后会发出 QThread::finished() 的信号,我们将这个信号和 deleteLater 槽绑定,线程结束后调用deleteLater来销毁分配的内存。
在MainWindow类构造函数中,添加以下代码:
connect(WorkerTh, &QThread::finished, WorkerTh, &QObject::deleteLater)
~MainWindow()析构函数可以把 wait()函数去掉了,因为该线程的资源已经不是让主窗口来接管了。当我们启动线程之后,然后退出主窗口或者直接点击【stop】+【exit()】按钮的时候,会出现以下的警告:
QThread::wait: Thread tried to wait on itself
QThread: Destroyed while thread is still running
为了让子线程能够响应信号并在子线程执行槽函数,我们在InheritQThread类构造函数中添加了 moveToThread(this) ,此方法是官方极其不推荐使用的方法。那么现在我们就遇到了由于这个方法引发的问题,我们把moveToThread(this)删除,程序就可以正常结束和释放资源了。那如果要让子线程能够响应信号并在子线程执行槽函数,这应该怎么做?在下面的章节会介绍一个官方推荐的《子类化QObject+moveToThread》的方法。
3.1.6 小结#
- QThread只有run函数是在新线程里;
- 如果必须需要实现在线程内执行槽的情景,那就需要在QThread的派生类构造函数中调用moveToThread(this),并且在run函数内执行QThread::exec()开启事件循环;(极其不推荐使用moveToThread(this),下一节会介绍一种安全可靠的方法)
- 若需要使用事件循环,需要在run函数中调用QThread::exec();
- 尽量不要使用terminate()来结束线程,可以使用bool标志位退出或者在线程处于事件循环时调用QThread::quit、QThread::exit来退出线程;
- 善用QObject::deleteLater来进行内存管理;
- 在QThread执行start函数之后,run函数还未运行完毕,再次start,不会发生任何结果;
- 子类化QThread多线程的方法适用于后台执行长时间的耗时操作、单任务执行的、无需在线程内执行槽的情景。
3.2 子类化QObject+moveToThread#
从QThread源码可知,在Qt4.4之前,run 是纯虚函数,必须子类化QThread来实现run函数。而从Qt4.4开始,QThread不再支持抽象类,run 默认调用 QThread::exec() ,不需要子类化QThread,只需要子类化一个QObject,通过QObject::moveToThread将QObject派生类移动到线程中即可。这是官方推荐的方法,而且使用灵活、简单、安全可靠。如果线程要用到事件循环,使用继承QObject的多线程方法无疑是一个更好的选择。
这个小节主要是说一下,子类化QObject+moveToThread的多线程使用方法以及一些注意问题,其中有很多细节的问题其实和《3.1 子类化QThread》这个小节是一样的,在这里就不再多说了,不明白的可以到上一节找找答案。
3.2.1 步骤#
- 写一个继承QObject的类,将需要进行复杂耗时的逻辑封装到槽函数中,作为线程的入口,入口可以有多个;
- 在旧线程创建QObject派生类对象和QThread对象,最好使用堆分配的方式创建(new),并且最好不要为此两个对象设置父类,便于后期程序的资源管理;
- 把obj通过moveToThread方法转移到新线程中,此时obj不能有任何的父类;
- 把线程的finished信号和obj对象、QThread对象的 QObject::deleteLater 槽连接,这个信号槽必须连接,否则会内存泄漏;如果QObject的派生类和QThread类指针是需要重复使用,那么就需要处理由对象被销毁之前立即发出的 QObject::destroyed 信号,将两个指针设置为nullptr,避免出现野指针;
- 将其他信号与QObject派生类槽连接,用于触发线程执行槽函数里的任务;
- 初始化完后调用 QThread::start() 来启动线程,默认开启事件循环;
- 在逻辑结束后,调用 QThread::quit 或者 QThread::exit 退出线程的事件循环。
3.2.2 实例#
写一个继承QObject的类:InheritQObject,代码如下:
#ifndef INHERITQOBJECT_H
#define INHERITQOBJECT_H
#include <QObject>
#include <QThread>
#include <QMutex>
#include <QMutexLocker>
#include <QDebug>
class InheritQObject : public QObject
{
Q_OBJECT
public:
explicit InheritQObject(QObject *parent = 0) : QObject(parent){
}
//用于退出线程循环计时的槽函数
void StopTimer(){
qDebug()<<"Exec StopTimer thread = "<<QThread::currentThreadId();
QMutexLocker lock(&m_lock);
m_flag = false;
}
signals:
void ValueChanged(int i);
public slots:
void QdebugSlot(){
qDebug()<<"Exec QdebugSlot thread = "<<QThread::currentThreadId();
}
//计时槽函数
void TimerSlot(){
qDebug()<<"Exec TimerSlot thread = "<<QThread::currentThreadId();
int i=0;
m_flag = true;
while(1)
{
++i;
emit ValueChanged(i);
QThread::sleep(1);
{
QMutexLocker lock(&m_lock);
if( !m_flag )
break;
}
}
}
private:
bool m_flag;
QMutex m_lock;
};
#endif // INHERITQOBJECT_H
mainwindow主窗口类,代码如下:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "ui_mainwindow.h"
#include "InheritQObject.h"
#include <QThread>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0) :
QMainWindow(parent),
ui(new Ui::MainWindow){
qDebug()<<"GUI thread = "<<QThread::currentThreadId();
ui->setupUi(this);
//创建QThread线程对象以及QObject派生类对象,注意:都不需要设置父类
m_th = new QThread();
m_obj = new InheritQObject();
//改变m_obj的线程依附关系
m_obj->moveToThread(m_th);
//释放堆空间资源
connect(m_th, &QThread::finished, m_obj, &QObject::deleteLater);
connect(m_th, &QThread::finished, m_th, &QObject::deleteLater);
//设置野指针为nullptr
connect(m_th, &QObject::destroyed, this, &MainWindow::SetPtrNullptr);
connect(m_obj, &QObject::destroyed, this, &MainWindow::SetPtrNullptr);
//连接其他信号槽,用于触发线程执行槽函数里的任务
connect(this, &MainWindow::StartTimerSignal, m_obj, &InheritQObject::TimerSlot);
connect(m_obj, &InheritQObject::ValueChanged, this, &MainWindow::setValue);
connect(this, &MainWindow::QdebugSignal, m_obj, &InheritQObject::QdebugSlot);
//启动线程,线程默认开启事件循环,并且线程正处于事件循环状态
m_th->start();
}
~MainWindow(){
delete ui;
}
signals:
void StartTimerSignal();
void QdebugSignal();
private slots:
//触发线程执行m_obj的计时槽函数
void on_startBt_clicked(){
emit StartTimerSignal();
}
//退出计时槽函数
void on_stopBt_clicked(){
m_obj->StopTimer();
}
//检测线程状态
void on_checkBt_clicked(){
if(m_th->isRunning()){
ui->label->setText("Running");
}else{
ui->label->setText("Finished");
}
}
void on_SendQdebugSignalBt_clicked(){
emit QdebugSignal();
}
//退出线程
void on_ExitBt_clicked(){
m_th->exit(0);
}
//强制退出线程
void on_TerminateBt_clicked(){
m_th->terminate();
}
//消除野指针
void SetPtrNullptr(QObject *sender){
if(qobject_cast<QObject*>(m_th) == sender){
m_th = nullptr;
qDebug("set m_th = nullptr");
}
if(qobject_cast<QObject*>(m_obj) == sender){
m_obj = nullptr;
qDebug("set m_obj = nullptr");
}
}
//响应m_obj发出的信号来改变时钟
void setValue(int i){
ui->lcdNumber->display(i);
}
private:
Ui::MainWindow *ui;
QThread *m_th;
InheritQObject *m_obj;
};
#endif // MAINWINDOW_H
通过以上的实例可以看到,我们无需重写 QThread::run 函数,也无需显式调用 QThread::exec 来启动线程的事件循环了,通过QT源码可以知道,只要调用 QThread::start 它就会自动执行 QThread::exec 来启动线程的事件循环。
第一种多线程的创建方法(继承QThread的方法),如果run函数里面没有死循环也没有调用exec开启事件循环的话,就算调用了 QThread::start 启动线程,最终过一段时间,线程依旧是会退出,处于finished的状态。那么这种方式会出现这样的情况吗?我们直接运行上面的实例,然后过段时间检查线程的状态:
发现线程是一直处于运行状态的。那接下来我们说一下应该怎么正确使用这种方式创建的线程并正确退出线程和释放资源。
3.2.3 如何正确使用线程(信号槽)和创建线程资源#
(1)如何正确使用线程?
如果需要让线程去执行一些行为,那就必须要正确使用信号槽的机制来触发槽函数,其他的方式调用槽函数都只是在旧线程中执行,无法达到预想效果。在多线程中信号槽的细节,会在《三、跨线程的信号槽》章节来讲解,这里我们先简单说如何使用信号槽来触发线程执行任务先。
通过以上的实例得知,MainWindow 构造函数中使用了connect函数将 StartTimerSignal() 信号和 InheritQObject::TimerSlot() 槽进行了绑定,代码语句如下:
connect(this, &MainWindow::StartTimerSignal, m_obj, &InheritQObject::TimerSlot);
当点击【startTime】按钮发出 StartTimerSignal() 信号时,这个时候就会触发线程去执行 InheritQObject::TimerSlot() 槽函数进行计时。
由上面的打印信息得知,InheritQObject::TimerSlot() 槽函数的确是在一个新的线程中执行了。在上面继承QThread的多线程方法中也有说到,在这个时候去执行QThread::exit或者是QThread::quit是无效的,退出的信号会一直挂在消息队列里,只有点击了【stopTime】按钮让线程退出 while 循环,并且线程进入到事件循环 ( exec() ) 中,才会生效,并退出线程。
如果将【startTime】按钮不是发出 StartTimerSignal() 信号,而是直接执行InheritQObject::TimerSlot() 槽函数,会是怎么样的结果呢?代码修改如下:
//触发线程执行m_obj的计时槽函数
void on_startBt_clicked(){
m_obj->TimerSlot();
}
我们会发现界面已经卡死,InheritQObject::TimerSlot() 槽函数是在GUI主线程执行的,这就导致了GUI界面的事件循环无法执行,也就是界面无法被更新了,所以出现了卡死的现象。所以要使用信号槽的方式来触发线程工作才是有效的,不能够直接调用obj里面的成员函数。
(2)如何正确创建线程资源?
有一些资源我们可以直接在旧线程中创建(也就是不通过信号槽启动线程来创建资源),在新线程也可以直接使用,例如实例中的bool m_flag和QMutex m_lock变量都是在就线程中定义的,在新线程也可以使用。但是有一些资源,如果你需要在新线程中使用,那么就必须要在新线程创建,例如定时器、网络套接字等,下面以定时器作为例子,代码按照下面修改:
/**********在InheritQObject类中添加QTimer *m_timer成员变量*****/
QTimer *m_timer;
/**********在InheritQObject构造函数创建QTimer实例*****/
m_timer = new QTimer();
/**********在InheritQObject::TimerSlot函数使用m_timer*****/
m_timer->start(1000);
运行点击【startTime】按钮的时候,会出现以下报错:
QObject::startTimer: Timers cannot be started from another thread
由此可知,QTimer是不可以跨线程使用的,所以将程序修改成如下,将QTimer的实例创建放到线程里面创建:
/*********在InheritQObject类中添加Init的槽函数,将需要初始化创建的资源放到此处********/
public slots:
void Init(){
m_timer = new QTimer();
}
/********在MainWindow类中添加InitSiganl()信号,并绑定信号槽***********/
//添加信号
signals:
void InitSiganl();
//在MainWindow构造函数添加以下代码
connect(this, &MainWindow::InitSiganl, m_obj, &InheritQObject::Init); //连接信号槽
emit InitSiganl(); //发出信号,启动线程初始化QTimer资源
这样QTimer定时器就属于新线程,并且可以正常使用啦。网络套接字QUdpSocket、QTcpSocket等资源同理处理就可以了。
3.2.4 如何正确退出线程并释放资源#
(1)如何正确退出线程?
正确退出线程的方式,其实和上面《3.1.5 如何正确退出线程并释放资源》小节所讲到的差不多,就是要使用 quit 和 exit 来退出线程,避免使用 terminate 来强制结束线程,有时候会出现异常的情况。例如以上的实例,启动之后,直接点击 【terminate】按钮,界面就会出现卡死的现象。
(2)如何正确释放线程资源?
在上面《3.1.5 如何正确退出线程并释放资源》小节也有讲到,千万别手动delete QThread类或者派生类的线程指针,手动delete会发生不可预料的意外。理论上所有QObject都不应该手动delete,如果没有多线程,手动delete可能不会发生问题,但是多线程情况下delete非常容易出问题,那是因为有可能你要删除的这个对象在Qt的事件循环里还排队,但你却已经在外面删除了它,这样程序会发生崩溃。所以需要 善用QObject::deleteLater 和 QObject::destroyed来进行内存管理。如上面实例使用到的代码:
//释放堆空间资源
connect(m_th, &QThread::finished, m_obj, &QObject::deleteLater);
connect(m_th, &QThread::finished, m_th, &QObject::deleteLater);
//设置野指针为nullptr
connect(m_th, &QObject::destroyed, this, &MainWindow::SetPtrNullptr);
connect(m_obj, &QObject::destroyed, this, &MainWindow::SetPtrNullptr);
//消除野指针
void SetPtrNullptr(QObject *sender){
if(qobject_cast<QObject*>(m_th) == sender){
m_th = nullptr;
qDebug("set m_th = nullptr");
}
if(qobject_cast<QObject*>(m_obj) == sender){
m_obj = nullptr;
qDebug("set m_obj = nullptr");
}
}
当我们调用线程的 quit 或者 exit 函数,并且线程到达了事件循环的状态,那么线程就会在结束并且发出 QThread::finished 的信号来触发 QObject::deleteLater 槽函数,QObject::deleteLater就会销毁系统为m_obj、m_th对象分配的资源。这个时候m_obj、m_th指针就属于野指针了,所以需要根据QObject类或者QObject派生类对象销毁时发出来的 QObject::destroyed 信号来设置m_obj、m_th指针为nullptr,避免野指针的存在。
运行上面的实例,然后点击【exit】按钮,结果如下图:
3.2.5 小结#
- 这种QT多线程的方法,实现简单、使用灵活,并且思路清晰,相对继承于QThread类的方式更有可靠性,这种方法也是官方推荐的实现方法。如果线程要用到事件循环,使用继承QObject的多线程方法无疑是一个更好的选择;
- 创建QObject派生类对象不能带有父类;
- 调用QThread::start是默认启动事件循环;
- 必须需要使用信号槽的方式使用线程;
- 需要注意跨线资源的创建,例如QTimer、QUdpSocket等资源,如果需要在子线程中使用,必须要在子线程创建;
- 要善用QObject::deleteLater 和 QObject::destroyed来进行内存管理 ;
- 尽量避免使用terminate强制退出线程,若需要退出线程,可以使用quit或exit;
3.3 继承QRunnable+QThreadPool#
此方法个人感觉使用的相对较少,在这里只是简单介绍下使用的方法。我们可以根据使用的场景来选择方法。
此方法和QThread的区别:
- 与外界通信方式不同。由于QThread是继承于QObject的,但QRunnable不是,所以在QThread线程中,可以直接将线程中执行的结果通过信号的方式发到主程序,而QRunnable线程不能用信号槽,只能通过别的方式,等下会介绍;
- 启动线程方式不同。QThread线程可以直接调用start()函数启动,而QRunnable线程需要借助QThreadPool进行启动;
- 资源管理不同。QThread线程对象需要手动去管理删除和释放,而QRunnable则会在QThreadPool调用完成后自动释放。
接下来就来看看QRunnable的用法、使用场景以及注意事项;
3.3.1 步骤#
要使用QRunnable创建线程,步骤如下:
- 继承QRunnable。和QThread使用一样, 首先需要将你的线程类继承于QRunnable;
- 重写run函数。还是和QThread一样,需要重写run函数;
- 使用QThreadPool启动线程。
3.3.2 实例#
继承于QRunnable的类:
#ifndef INHERITQRUNNABLE_H
#define INHERITQRUNNABLE_H
#include <QRunnable>
#include <QWidget>
#include <QDebug>
#include <QThread>
class CusRunnable : public QRunnable
{
public:
explicit CusRunnable(){
}
~CusRunnable(){
qDebug() << __FUNCTION__;
}
void run(){
qDebug() << __FUNCTION__ << QThread::currentThreadId();
QThread::msleep(1000);
}
};
#endif // INHERITQRUNNABLE_H
主界面类:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "ui_mainwindow.h"
#include "InheritQRunnable.h"
#include <QThreadPool>
#include <QDebug>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0) :
QMainWindow(parent),
ui(new Ui::MainWindow){
ui->setupUi(this);
m_pRunnable = new CusRunnable();
qDebug() << __FUNCTION__ << QThread::currentThreadId();
QThreadPool::globalInstance()->start(m_pRunnable);
}
~MainWindow(){
qDebug() << __FUNCTION__ ;
delete ui;
}
private:
Ui::MainWindow *ui;
CusRunnable * m_pRunnable = nullptr;
};
#endif // MAINWINDOW_H
直接运行以上实例,结果输出如下:
MainWindow 0x377c
run 0x66ac
~CusRunnable
我们可以看到这里打印的线程ID是不同的,说明是在不同线程中执行,而线程执行完后就自动进入到析构函数中, 不需要手动释放。
3.3.3 启动线程的方式#
上面我们说到要启动QRunnable线程,需要QThreadPool配合使用,而调用方式有两种:全局线程池和非全局线程池。
(1)使用全局线程池启动
QThreadPool::globalInstance()->start(m_pRunnable);
(2)使用非全局线程池启动
该方式可以控制线程最大数量, 以及其他设置,比较灵活,具体参照帮助文档。
QThreadPool threadpool;
threadpool.setMaxThreadCount(1);
threadpool.start(m_pRunnable);
3.3.4 如何与外界通信#
前面我们提到,因为QRunnable没有继承于QObject,所以没法使用信号槽与外界通信,那么,如果要在QRunnable线程中和外界通信怎么办呢,通常有两种做法:
- 使用多继承。让我们的自定义线程类同时继承于QRunnable和QObject,这样就可以使用信号和槽,但是多线程使用比较麻烦,特别是继承于自定义的类时,容易出现接口混乱,所以在项目中尽量少用多继承。
- 使用QMetaObject::invokeMethod。
接下来只介绍使用QMetaObject::invokeMethod来通信:
QMetaObject::invokeMethod 函数定义如下:
static bool QMetaObject::invokeMethod(
QObject *obj, const char *member,
Qt::ConnectionType,
QGenericReturnArgument ret,
QGenericArgument val0 = QGenericArgument(Q_NULLPTR),
QGenericArgument val1 = QGenericArgument(),
QGenericArgument val2 = QGenericArgument(),
QGenericArgument val3 = QGenericArgument(),
QGenericArgument val4 = QGenericArgument(),
QGenericArgument val5 = QGenericArgument(),
QGenericArgument val6 = QGenericArgument(),
QGenericArgument val7 = QGenericArgument(),
QGenericArgument val8 = QGenericArgument(),
QGenericArgument val9 = QGenericArgument());
该函数就是尝试调用obj的member函数,可以是信号、槽或者Q_INVOKABLE声明的函数(能够被Qt元对象系统唤起),只需要将函数的名称传递给此函数,调用成功返回true,失败返回false。member函数调用的返回值放在ret中,如果调用是异步的,则不能计算返回值。你可以将最多10个参数(val0、val1、val2、val3、val4、val5、val6、val7、val8和val9)传递给member函数,必须使用Q_ARG()和Q_RETURN_ARG()宏封装参数,Q_ARG()接受类型名 + 该类型的常量引用;Q_RETURN_ARG()接受一个类型名 + 一个非常量引用。
QMetaObject::invokeMethod可以是异步调用,也可以是同步调用。这取决与它的连接方式Qt::ConnectionType type:
- 如果类型是Qt::DirectConnection,则会立即调用该成员,同步调用。
- 如果类型是Qt::QueuedConnection,当应用程序进入主事件循环时,将发送一个QEvent并调用该成员,异步调用。
- 如果类型是Qt::BlockingQueuedConnection,该方法将以与Qt::QueuedConnection相同的方式调用,不同的地方:当前线程将阻塞,直到事件被传递。使用此连接类型在同一线程中的对象之间通信将导致死锁。
- 如果类型是Qt::AutoConnection,如果obj与调用者在同一线程,成员被同步调用;否则,它将异步调用该成员。
我们在主界面中定一个函数,用于更新界面内容:
Q_INVOKABLE void setText(QString msg){
ui->label->setText(msg);
}
继承于QRunnable的线程类,修改完成如下:
#ifndef INHERITQRUNNABLE_H
#define INHERITQRUNNABLE_H
#include <QRunnable>
#include <QWidget>
#include <QDebug>
#include <QThread>
class CusRunnable : public QRunnable
{
public:
//修改构造函数
explicit CusRunnable(QObject *obj):m_pObj(obj){
}
~CusRunnable(){
qDebug() << __FUNCTION__;
}
void run(){
qDebug() << __FUNCTION__ << QThread::currentThreadId();
QMetaObject::invokeMethod(m_pObj,"setText",Q_ARG(QString,"hello world!")); //此处与外部通信
QThread::msleep(1000);
}
private:
QObject * m_pObj = nullptr; //定义指针
};
#endif // INHERITQRUNNABLE_H
创建线程对象时,需要将主界面对象传入线程类,如下:
m_pRunnable = new CusRunnable(this);
到这里也就实现了线程与外部通信了,运行效果如下:
3.3.5 小结#
- 使用该方法实现的多线程,线程中的资源无需用户手动释放,线程执行完后会自动回收资源;
- 和继承QThread的方法一样需要继承类,并且重新实现run函数;
- 需要结合QThreadPool线程池来使用;
- 与外界通信可以使用如果使用信号槽机制会比较麻烦,可以使用QMetaObject::invokeMethod的方式与外界通信。
3.4 QtConcurrent::run()+QThreadPool#
在QT开发的场景中,个人觉得此方法使用的也比较少,所以本文只作一个简单使用的介绍。QtConcurrent 是命名空间 (namespace),它提供了高层次的函数接口 (APIs),使所写程序,可根据计算机的 CPU 核数,自动调整运行的线程数目。本文以 Qt 中的 QtConcurrent::run() 函数为例,介绍如何将函数运行在单独的线程中。
(1)使用 QtConcurrent 模块,需要在 .pro 中添加:
QT += concurrent
(2)将一个普通函数运行在单独线程:
#include <QApplication>
#include <QDebug>
#include <QThread>
#include <QtConcurrent>
void fun1(){
qDebug()<<__FUNCTION__<<QThread::currentThread();
}
void fun2(QString str1, QString str2){
qDebug()<<__FUNCTION__<<str1+str2<<QThread::currentThread();
}
int fun3(int i, int j){
qDebug()<<__FUNCTION__<<QThread::currentThread();
return i+j;
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
qDebug()<<__FUNCTION__<<QThread::currentThread();
//无参数的普通函数
QFuture<void> fut1 = QtConcurrent::run(fun1);
//有参数的普通函数
QFuture<void> fut2 = QtConcurrent::run(fun2, QString("Thread"),QString(" 2"));
//获取普通函数的返回值
int i=1, j=2;
QFuture<int> fut3 = QtConcurrent::run(fun3, i, j);
qDebug()<<"ret:"<<fut3.result();
//以上的例子,如果要为其指定线程池,可以将线程池的指针作为第一个参数传递进去
QThreadPool pool;
QFuture<void> fut4 = QtConcurrent::run(&pool, fun1);
fut1.waitForFinished();
fut2.waitForFinished();
fut3.waitForFinished();
fut4.waitForFinished();
return a.exec();
}
输出结果:
qMain QThread(0xf380590)
fun2 "Thread 2" QThread(0x1ca7c758, name = "Thread (pooled)")
fun1 QThread(0x1ca7c6d8, name = "Thread (pooled)")
fun3 QThread(0x1ca7c5b8, name = "Thread (pooled)")
ret: 3
fun1 QThread(0x1ca7c438, name = "Thread (pooled)")
(3)将类中的成员函数单独运行在线程中:
将类中的成员函数运行在某一个线程中,可将指向该类实例的引用或指针作为 QtConcurrent::run 的第一个参数传递进去,常量成员函数一般传递常量引用 (const reference),而非常量成员函数一般传递指针 (pointer)。
- 常量成员函数
在一个单独的线程中,调用 QByteArray 的常量成员函数 split(),传递给 run() 函数的参数是 bytearray
//常量成员函数QByteArray::split()
QByteArray bytearray = "hello,world";
QFuture<QList<QByteArray> > future = QtConcurrent::run(bytearray, &QByteArray::split, ',');
QList<QByteArray> result = future.result();
qDebug()<<"result:"<<result;
- 非常量成员函数
在一个单独的线程中,调用 QImage 的非常量成员函数 invertPixels(),传递给 run() 函数的参数是 &image
QImage image = ...;
QFuture<void> future = QtConcurrent::run(&image, &QImage::invertPixels, QImage::InvertRgba);
...
future.waitForFinished(); // At this point, the pixels in 'image' have been inverted
四、跨线程的信号槽#
线程的信号槽机制需要开启线程的事件循环机制,即调用QThread::exec()函数开启线程的事件循环。
Qt信号-槽连接函数原型如下:
bool QObject::connect ( const QObject * sender, const char * signal, const QObject * receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection )
Qt支持5种连接方式
- Qt::DirectConnection(直连方式)(信号与槽函数关系类似于函数调用,同步执行)
当信号发出后,相应的槽函数将立即被调用。emit语句后的代码将在所有槽函数执行完毕后被执行。当信号发射时,槽函数将直接被调用。无论槽函数所属对象在哪个线程,槽函数都在发射信号的线程内执行。 - Qt::QueuedConnection(队列方式)(此时信号被塞到事件队列里,信号与槽函数关系类似于消息通信,异步执行)
当信号发出后,排队到信号队列中,需等到接收对象所属线程的事件循环取得控制权时才取得该信号,调用相应的槽函数。emit语句后的代码将在发出信号后立即被执行,无需等待槽函数执行完毕。当控制权回到接收者所依附线程的事件循环时,槽函数被调用。槽函数在接收者所依附线程执行。 - Qt::AutoConnection(自动方式)
Qt的默认连接方式,如果信号的发出和接收信号的对象同属一个线程,那个工作方式与直连方式相同;否则工作方式与队列方式相同。如果信号在接收者所依附的线程内发射,则等同于直接连接如果发射信号的线程和接受者所依附的线程不同,则等同于队列连接 - Qt::BlockingQueuedConnection(信号和槽必须在不同的线程中,否则就产生死锁)
槽函数的调用情形和Queued Connection相同,不同的是当前的线程会阻塞住,直到槽函数返回。 - Qt::UniqueConnection
与默认工作方式相同,只是不能重复连接相同的信号和槽,因为如果重复连接就会导致一个信号发出,对应槽函数就会执行多次。
如果没有特殊的要求我们connect函数选择默认的连接方式就好,也就是connect的第五个参数不填写就ok,例如:
connect(m_obj, &QObject::destroyed, this, &MainWindow::SetPtrNullptr);
五、总结#
本文章分析了部分QThread源码,讲解了四种QT多线程的实现方法,以及多线程信号槽连接的知识点。接下来我再简单对以上四种QT多线程的实现方法,总结一下哪种情况该使用哪种 Qt 线程技术:
需要线程的生命周期 | 开发场景 | 解决方案 |
---|---|---|
单次调用 | 在其他的线程中运行一个方法,当方法运行结束后退出线程。 | (1)编写一个函数,然后利用 QtConcurrent::run()运行它;(2)从QRunnable 派生一个类,并利用全局线程池QThreadPool::globalInstance()->start()来运行它。(3) 从QThread派生一个类, 重载QThread::run() 方法并使用QThread::start()来运行它。 |
单次调用 | 一个耗时的操作必须放到另一个线程中运行。在这期间,状态信息必须发送到GUI线程中。 | 使用 QThread,,重载run方法并根据情况发送信号。.使用queued信号/槽连接来连接信号与GUI线程的槽。 |
常驻 | 有一对象位于另一个线程中,将让其根据不同的请求执行不同的操作。这意味与工作者线程之间的通信是必须的。 | 从QObject 派生一个类并实现必要的槽和信号,将对象移到一个具有事件循环的线程中,并通过queued信号/槽连接与对象进行通信。 |
当然QT还有其他实现多线程的方法,例如使用QtConcurrent::map()函数、QSocketNotifier,具体怎么使用,这里就不再过多介绍了。
这个是本文章实例的源码地址:https://gitee.com/CogenCG/QThreadExample.git
=====================================
Qt 的4种多线程实现详解 (** 含测试代码)
为何需要多线程?
1、进行耗时操作时,可以处理用户的其他输入输出。比如,如果在UI线程里面进行耗时操作,界面会不响应用户操作。
2、提升程序性能。现在的电脑一般都是多核CPU,多线程并行处理事务,可以大大提升程序的性能。
针对第一点,为我们定位界面不响应问题指明了一个方向;针对第二点,为我们提升软件处理效率指明了一个方向。
那么,基于Qt开发的应用程序,如何实现多线程呢?
目录
- 1、继承QThread,重载run函数。
- 2、继承QObject,调用void QObject::moveToThread(QThread *targetThread)。
- 3、QThreadPool and QRunnabl。
- 4、Qt Concurrent。
- 5、测试代码
使用多线程之前,特别需要注意的一点是:
非UI线程 不能操作UI对象(从QWidget直接或间接派生的窗口对象)
1、继承QThread,
重载run函数。
这种方法比较适用于处理耗时很长的业务。示例代码如下:
class WorkerThread : public QThread
{
Q_OBJECT
void run() override {
QString result;
/* ... here is the expensive or blocking operation ... */
emit resultReady(result);
}
signals:
void resultReady(const QString &s);
};
void MyObject::startWorkInAThread()
{
WorkerThread *workerThread = new WorkerThread(this);
connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults);
connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
workerThread->start();
}
需要注意以下几点:
1)run函数在新线程中执行,run函数执行结束,线程结束。
2)WorkerThread实例化的对象属于创建他的线程,而不是run函数所在线程。
3)WorkerThread没有事件循环,除非在run()函数中调用exec();
4)队列连接到WorkerThread的slot函数,slot函数在创建WorkerThread对象的线程中执行。
5)直接调用WorkerThread的方法,该方法的执行线程为调用处的线程。
2、继承QObject,调用
void QObject::moveToThread(QThread *targetThread)。
这种方法适用于在一个类中处理多个耗时任务,且 这个些任务不会并行执行的情况。示例代码如下:
class Worker : public QObject
{
Q_OBJECT
public slots:
void doWork(const QString ¶meter) {
QString result;
/* ... here is the expensive or blocking operation ... */
emit resultReady(result);
}
signals:
void resultReady(const QString &result);
};
class Controller : public QObject
{
Q_OBJECT
QThread workerThread;
public:
Controller() {
Worker *worker = new Worker;
worker->moveToThread(&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 &);
};
需要注意以下几点:
1)调用moveToThread函数的对象不能设置父对象。
2)Worker类中的槽函数可以跟任意线程的任意信号建立连接,队列连接时,在新线程中执行。
3)直接调用Worker类中的函数,在调用线程内执行。
4)同时发送多个与Worker类中槽函数连接的信号,槽函数依次执行。
3、QThreadPool and QRunnabl。
我们都知道频繁创建和销毁线程会带来较大的性能开销,影响程序执行效率。Qt的线程池技术,给了我们一个解决这个问题的有效方法。示例代码如下:
class HelloWorldTask : public QRunnable
{
void run() override
{
qDebug() << "Hello world from thread" << QThread::currentThread();
}
};
HelloWorldTask *hello = new HelloWorldTask();
// QThreadPool takes ownership and deletes 'hello' automatically
QThreadPool::globalInstance()->start(hello);
注意:
1)默认情况下,run函数执行完,hello对象会被线程池自动删除。可以使用setAutoDelete函数设置。
2)QThreadPool::start()多次启动设置为autoDelete的QRunnable对象,可能导致崩溃。
4、Qt Concurrent。
QtConcurrent提供了高级api,使编写多线程程序时,不需要使用诸如互斥锁、读写锁、等待条件或信号量等低级线程安全类。具体用法,我们下回分解。传送门:Qt Concurrent 线程使用详解
Concurrent Run
extern void aFunction();
QFuture<void> future = QtConcurrent::run(aFunction);Concurrent Map and Map-Reduce
QList<QImage> images = ...;
// Each call blocks until the entire operation is finished.
QList<QImage> future = QtConcurrent::blockingMapped(images, scaled);
QtConcurrent::blockingMap(images, scale);
QImage collage = QtConcurrent::blockingMappedReduced(images, scaled, addToCollage);Concurrent Filter and Filter-Reduce
QStringList strings = ...;
// each call blocks until the entire operation is finished
QStringList lowerCaseStrings = QtConcurrent::blockingFiltered(strings, allLowerCase);
QtConcurrent::blockingFilter(strings, allLowerCase);
QSet<QString> dictionary = QtConcurrent::blockingFilteredReduced(strings, allLowerCase, addToDictionary);
5、测试代码
main.cpp
#include "qtthreaddemo.h"
#include <QtWidgets/QApplication>
#include <QDebug>
#include <QThread>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
qDebug() << "Main Thread Id:" << QThread::currentThreadId();
QtThreadDemo w;
w.show();
return a.exec();
}
qtthreaddemo.h
#pragma once
#include <QtWidgets/QWidget>
#include "ui_qtthreaddemo.h"
class WorkerThread1;
class WorkerThread2;
class QtThreadDemo : public QWidget
{
Q_OBJECT
public:
QtThreadDemo(QWidget *parent = Q_NULLPTR);
private slots:
void on_btnCreateThread1_clicked();
void on_btnSlotFun_clicked();
void on_btnCreateThread2_clicked();
void on_btnTestOrders_clicked();
void on_btnRunnableThread_clicked();
void workerThreadEnd();
signals:
void run_slot();
private:
Ui::QtThreadDemoClass ui;
WorkerThread1 *m_pThread1 = nullptr;
WorkerThread2 *m_pThread2 = nullptr;
};
qtthreaddemo.cpp
#include "qtthreaddemo.h"
#include "workerthread1.h"
#include "workerthread2.h"
#include "runnablethread.h"
#include <QDebug>
#include <QThreadPool>
QtThreadDemo::QtThreadDemo(QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);
}
void QtThreadDemo::on_btnCreateThread1_clicked()
{
qDebug() << "btnCreateThread clicked Thread Id:" << QThread::currentThreadId();
m_pThread1 = new WorkerThread1(this);
connect(this, SIGNAL(run_slot()), m_pThread1, SLOT(doJob()), Qt::QueuedConnection);
connect(m_pThread1, SIGNAL(finished()), this, SLOT(workerThreadEnd()));
m_pThread1->start();
}
void QtThreadDemo::on_btnSlotFun_clicked()
{
qDebug() << "btnSlotFun clicked Thread Id:" << QThread::currentThreadId();
emit run_slot();
}
void QtThreadDemo::on_btnCreateThread2_clicked()
{
qDebug() << "btnCreateThread2 clicked";
if (m_pThread2 == nullptr)
{
m_pThread2 = new WorkerThread2(nullptr);
}
m_pThread2->doJob();
}
void QtThreadDemo::on_btnTestOrders_clicked()
{
qDebug() << "btnTestOrders clicked";
if (m_pThread2 == nullptr)
{
m_pThread2 = new WorkerThread2(nullptr);
}
m_pThread2->testOrder();
}
void QtThreadDemo::on_btnRunnableThread_clicked()
{
qDebug() << "btnRunnableThread clicked";
RunnableThread *pThread = new RunnableThread;
QThreadPool::globalInstance()->tryStart(pThread);
QThread::msleep(2000);
//QThreadPool::globalInstance()->tryStart(pThread); //会发生异常
}
void QtThreadDemo::workerThreadEnd()
{
qDebug() << "Worker Thread Ended!";
}
workthread1.h
#pragma once
#include <QThread>
class WorkerThread1 : public QThread
{
Q_OBJECT
public:
WorkerThread1(QObject *parent);
~WorkerThread1();
static void publicFun();
protected:
void run();
private slots:
void doJob();
};
workerthread1.cpp
#include "workerthread1.h"
#include <QDebug>
WorkerThread1::WorkerThread1(QObject *parent)
: QThread(parent)
{
qDebug() << "WorkerThread1 Object Thread Id:" << QThread::currentThreadId();
}
WorkerThread1::~WorkerThread1()
{
}
void WorkerThread1::publicFun()
{
qDebug() << "WorkerThread1::publicFun Thread Id:" << QThread::currentThreadId();
}
void WorkerThread1::run()
{
qDebug() << "WorkerThread1 Create Thread Id:" << QThread::currentThreadId();
}
void WorkerThread1::doJob()
{
qDebug() << "WorkerThread1 Slot-doJob Thread Id:" << QThread::currentThreadId();
}
workerthread2.h
#pragma once
#include <QObject>
class QThread;
class WorkerThread2 : public QObject
{
Q_OBJECT
public:
WorkerThread2(QObject *parent);
~WorkerThread2();
void doJob();
void testOrder();
private slots:
void onDoJob();
void onOrder1();
void onOrder2();
void onOrder3();
signals:
void sig_do_job();
void sig_order1();
void sig_order2();
void sig_order3();
private:
QThread *m_pThread;
};
workerthread2.cpp
#include "workerthread2.h"
#include <QThread>
#include <QDebug>
#include "workerthread1.h"
WorkerThread2::WorkerThread2(QObject *parent)
: QObject(parent)
{
m_pThread = new QThread;
moveToThread(m_pThread);
m_pThread->start();
connect(this, SIGNAL(sig_do_job()), this, SLOT(onDoJob()), Qt::QueuedConnection);
connect(this, SIGNAL(sig_order1()), this, SLOT(onOrder1()), Qt::QueuedConnection);
connect(this, SIGNAL(sig_order2()), this, SLOT(onOrder2()), Qt::QueuedConnection);
connect(this, SIGNAL(sig_order3()), this, SLOT(onOrder3()), Qt::QueuedConnection);
}
WorkerThread2::~WorkerThread2()
{
}
void WorkerThread2::doJob()
{
qDebug() << "WorkerThread2::doJob thread id:" << QThread::currentThreadId();
emit sig_do_job();
}
void WorkerThread2::testOrder()
{
emit sig_order1();
emit sig_order2();
emit sig_order3();
}
void WorkerThread2::onDoJob()
{
qDebug() << "WorkerThread2::onDoJob thread id:" << QThread::currentThreadId();
WorkerThread1::publicFun();
}
void WorkerThread2::onOrder1()
{
qDebug() << "WorkerThread2::onOrder1 begin";
QThread::msleep(1000);
qDebug() << "WorkerThread2::onOrder1 end";
}
void WorkerThread2::onOrder2()
{
qDebug() << "WorkerThread2::onOrder2 begin";
QThread::msleep(1000);
qDebug() << "WorkerThread2::onOrder2 end";
}
void WorkerThread2::onOrder3()
{
qDebug() << "WorkerThread2::onOrder3 begin";
QThread::msleep(1000);
qDebug() << "WorkerThread2::onOrder3 end";
}
runnablethread.h
#pragma once
#include <QRunnable>
class RunnableThread : public QRunnable
{
public:
RunnableThread();
~RunnableThread();
void run();
};
runnablethread.cpp
#include "runnablethread.h"
#include <QDebug>
#include <QThread>
RunnableThread::RunnableThread()
{
}
RunnableThread::~RunnableThread()
{
}
void RunnableThread::run()
{
qDebug() << "RunnableThread::run Thread Id:" << QThread::currentThreadId();
}
运行结果:
————————————————
版权声明:本文为CSDN博主「MrMa7ic」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/mrmagicleo/article/details/113761429
QtConcurrent 线程使用详解
关于Qt Concurrent,我们首先来看看Qt Assitant是怎么描述的。
The QtConcurrent namespace provides high-level APIs that make it possible to write multi-threaded programs without using low-level threading primitives such as mutexes, read-write locks, wait conditions, or semaphores. Programs written with QtConcurrent automatically adjust the number of threads used according to the number of processor cores available. This means that applications written today will continue to scale when deployed on multi-core systems in the future.
说简单点,就是我们使用QtConcurrent 实现多线程时,可以不用考虑对共享数据的保护问题。而且,它可以根据CPU的能力,自动优化线程数。
QtConcurrent 主要提供了3种实现多线程的方法,分别是:run,map,filter。下面分别进行详细说明使用方法。
目录
Concurrent run,在线程池内起一个线程来执行一个函数。
Concurrent map, 用于并行处理一批数据的场景。
Concurrent Filter,顾名思义,一般用于对一批数据的过滤操作。
Concurrent run,在线程池内起一个线程来执行一个函数。基本用法如下:
extern void aFunction();
QFuture<void> future = QtConcurrent::run(aFunction);
//std::thread 实现
std::thread thread = std::thread(aFunction);
thread.detach();
如上,标准库的thread 也能实现相同功能。不同的是,QtConcurrent 实现的线程,可以获取到线程函数的返回值。
extern QString functionReturningAString();
QFuture<QString> future = QtConcurrent::run(functionReturningAString);
...
QString result = future.result();
QtConcurrent::run() 也提供了线程函数传参的实现:
extern QString someFunction(const QByteArray &input);
QByteArray bytearray = ...;
QFuture<QString> future = QtConcurrent::run(someFunction, bytearray);
...
QString result = future.result();
另外,同std::thread, QtConcurrent::run() 也提供了成员函数实现线程的方案。
1)调用const 函数
QByteArray bytearray = "hello world";
QFuture<QList<QByteArray> > future = QtConcurrent::run(bytearray, &QByteArray::split, ',');或,
QFuture<QList<QByteArray> > future = QtConcurrent::run(&bytearray, &QByteArray::split, ',');
2)调用非const 函数
QImage image = ...;
QFuture<void> future = QtConcurrent::run(&image, &QImage::invertPixels, QImage::InvertRgba);
Concurrent map, 用于并行处理一批数据的场景。主要包含map, mapped, mappedReduced 三种用法。
map 函数用于需要更改原数据的使用场景,使用方法如下:
void toUpper(QString &str)
{
str = str.toUpper();
}
QStringList strWords;
strWords << "Apple" << "Banana" << "cow" << "dog" << "Egg";
auto future = QtConcurrent::map(strWords, toUpper);
future.waitForFinished();
//strWords = {"APPLE", "BANANA", "COW", "DOG", "EGG"}
mapped 函数用于不改变原数据,返回处理结果的使用场景。使用方法如下:
QString toUpper(const QString &str)
{
return str.toUpper();
}
QStringList strWords;
strWords << "Apple" << "Banana" << "cow" << "dog" << "Egg";
auto future = QtConcurrent::mapped(strWords, toUpper);
future.waitForFinished();
qDebug() << future.results();
//输出:("APPLE", "BANANA", "COW", "DOG", "EGG")
mappedReduced 用于mapped处理后的结果还需要进行处理的使用场景。使用方法如下:
QString toUpper(const QString &str)
{
return str.toUpper();
}
void reduceFun(QList<QString> &dictionary, const QString &string)
{
dictionary.push_back(QString("result: ") + string);
}
QStringList strWords;
strWords << "Apple" << "Banana" << "cow" << "dog" << "Egg";
auto future = QtConcurrent::mappedReduced(strWords, toUpper, reduceFun);
future.waitForFinished();
qDebug() << future.result();
//输出:("result: BANANA", "result: APPLE", "result: COW", "result: DOG", "result: EGG")
注意,上述代码输处结果的顺序与原数据的顺序已经不一样了。mappedReduced 函数的逻辑是,启动多个线程执行toUper 对链表中的每个元素进行处理,处理后的结果再逐一交给reduceFun函数处理。reduceFun 并不会等toUper的所有线程执行完毕后才开始执行,并且,同一时刻,只有一个reduceFun 线程在执行。如果需要使最后的输出结果顺序与输入相一致,就要用到mappedReduced 函数的第四个参数,赋值为QtConcurrent::OrderedReduce, 即可保证顺序一致。
同样的,Concurrent map也提供了成员函数作为线程函数的使用形式。线程函数必须是序列中元素的类型的成员函数。Qt 帮助文档中给出了如下示例:
// Squeeze all strings in a QStringList.
QStringList strings = ...;
QFuture<void> squeezedStrings = QtConcurrent::map(strings, &QString::squeeze);
// Swap the rgb values of all pixels on a list of images.
QList<QImage> images = ...;
QFuture<QImage> bgrImages = QtConcurrent::mapped(images, &QImage::rgbSwapped);
// Create a set of the lengths of all strings in a list.
QStringList strings = ...;
QFuture<QSet<int> > wordLengths = QtConcurrent::mappedReduced(strings, &QString::length, &QSet<int>::insert);
Concurrent map 还可以使用函数对象。Qt 帮助文档中给出了如下示例:
struct Scaled
{
Scaled(int size): m_size(size) { }
typedef QImage result_type;
QImage operator()(const QImage &image)
{
return image.scaled(m_size, m_size);
}
int m_size;
};
QList<QImage> images = ...;
QFuture<QImage> thumbnails = QtConcurrent::mapped(images, Scaled(100));
Concurrent Filter,顾名思义,一般用于对一批数据的过滤操作。同样也包含filter, filtered, filteredReduce 三种用法。
filter 函数必须按如下形式定义,T类型与处理数据的元素类型一致,返回false时,过滤掉相应的元素。
bool function(const T &t);
与Concurrent map 类似,filter 函数会改变原始数据,filtered 函数将处理结果保存在filtered 函数的返回值中,filteredReduce 会将过滤后的数据再调用reduceFun 函数处理。这里就不再进行详细赘述,可以完全参考map函数的用法使用。
————————————————
版权声明:本文为CSDN博主「MrMa7ic」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/mrmagicleo/article/details/115217760