【QT】子类化QObject+moveToThread实现多线程
往期链接:
从往期《QThread源码浅析》可知,在Qt4.4之前,run 是纯虚函数,必须子类化QThread来实现run函数。而从Qt4.4开始,QThread不再支持抽象类,run 默认调用 QThread::exec() ,不需要子类化QThread,只需要子类化一个QObject,通过QObject::moveToThread将QObject派生类移动到线程中即可。这是官方推荐的方法,而且使用灵活、简单、安全可靠。如果线程要用到事件循环,使用继承QObject的多线程方法无疑是一个更好的选择。
这一期主要是说一下,子类化QObject+moveToThread的多线程使用方法以及一些注意问题,其中有很多细节的问题其实和往期《子类化QThread实现多线程》文章是一样的,在这里就不再多说了,不明白的可以到往期《子类化QThread实现多线程》文章找找答案。
一、步骤
- 写一个继承QObject的类,将需要进行复杂耗时的逻辑封装到槽函数中,作为线程的入口,入口可以有多个;
- 在旧线程创建QObject派生类对象和QThread对象,最好使用堆分配的方式创建(new),并且最好不要为此两个对象设置父类,便于后期程序的资源管理;
- 把obj通过moveToThread方法转移到新线程中,此时obj不能有任何的父类;
- 把线程的finished信号和obj对象、QThread对象的 QObject::deleteLater 槽连接,这个信号槽必须连接,否则会内存泄漏;如果QObject的派生类和QThread类指针是需要重复使用,那么就需要处理由对象被销毁之前立即发出的 QObject::destroyed 信号,将两个指针设置为nullptr,避免出现野指针;
- 将其他信号与QObject派生类槽连接,用于触发线程执行槽函数里的任务;
- 初始化完后调用 QThread::start() 来启动线程,默认开启事件循环;
- 在逻辑结束后,调用 QThread::quit 或者 QThread::exit 退出线程的事件循环。
二、实例
写一个继承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的状态。那么这种方式会出现这样的情况吗?我们直接运行上面的实例,然后过段时间检查线程的状态:
发现线程是一直处于运行状态的。那接下来我们说一下应该怎么正确使用这种方式创建的线程并正确退出线程和释放资源。
三、如何正确使用线程(信号槽)和创建线程资源
(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等资源同理处理就可以了。
四、如何正确退出线程并释放资源
(1)如何正确退出线程?
正确退出线程的方式,其实和往期《子类化QThread实现多线程》中《如何正确退出线程并释放资源》小节所讲到的差不多,就是要使用 quit 和 exit 来退出线程,避免使用 terminate 来强制结束线程,有时候会出现异常的情况。例如以上的实例,启动之后,直接点击 【terminate】按钮,界面就会出现卡死的现象。
(2)如何正确释放线程资源?
在往期《子类化QThread实现多线程》中《如何正确退出线程并释放资源》小节也有讲到,千万别手动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】按钮,结果如下图:
五、小结
- 这种QT多线程的方法,实现简单、使用灵活,并且思路清晰,相对继承于QThread类的方式更有可靠性,这种方法也是官方推荐的实现方法。如果线程要用到事件循环,使用继承QObject的多线程方法无疑是一个更好的选择;
- 创建QObject派生类对象不能带有父类;
- 调用QThread::start是默认启动事件循环;
- 必须需要使用信号槽的方式使用线程;
- 需要注意跨线资源的创建,例如QTimer、QUdpSocket等资源,如果需要在子线程中使用,必须要在子线程创建;
- 要善用QObject::deleteLater 和 QObject::destroyed来进行内存管理 ;
- 尽量避免使用terminate强制退出线程,若需要退出线程,可以使用quit或exit;
【QT】子类化QObject+moveToThread实现多线程的更多相关文章
- Qt子类化后qss设置背景色无效的问题
1.问题背景 在某个类中,用到了一个组合的widget,有按钮进度条等,类似于视频播放器按钮控制区和精度条(参考了很多feiyangqingyun的文章,感谢),调试正常后整理代码,为了提高代码可读性 ...
- 【QT】继承QRunnable+QThreadPool实现多线程
往期链接: <QThread源码浅析> <子类化QThread实现多线程> <子类化QObject+moveToThread实现多线程> 本文章实例的源码地址:ht ...
- 【QT】QtConcurrent::run()+QThreadPool实现多线程
往期链接: <QThread源码浅析> <子类化QThread实现多线程> <子类化QObject+moveToThread实现多线程> <继承QRunnab ...
- 【QT】子类化QThread实现多线程
<QThread源码浅析> 子类化QThread来实现多线程, QThread只有run函数是在新线程里的,其他所有函数都在QThread生成的线程里.正确启动线程的方法是调用QThrea ...
- Qt ------ Thread Affinity (QObject::moveToThread: Cannot move objects with a parent)
简单的说,每个QObject的对象,都和某个创建对象所在的线程关联,如果把对象通过 moveToThread 移动到其他线程,这个对象不能有父对象,否则会出现 QObject::moveToThrea ...
- 重点:怎样正确的使用QThread类(注:包括推荐使用QThread线程的新方法QObject::moveToThread)
背景描述: 以前,继承 QThread 重新实现 run() 函数是使用 QThread唯一推荐的使用方法.这是相当直观和易于使用的.但是在工作线程中使用槽机制和Qt事件循环时,一些用户使用错了.Qt ...
- QThread 与 QObject的关系(QObject可以用于多线程,可以发送信号调用存在于其他线程的slot函数,但GUI类不可重入)
QThread 继承 QObject..它可以发送started和finished信号,也提供了一些slot函数. QObject.可以用于多线程,可以发送信号调用存在于其他线程的slot函数,也可以 ...
- 子类化QTreeWidgetItem实现增加Item的属性
因为有需求是点击QTreeWidgetItem需要获取该Item的节点的相关属性,Item需要保存关联的属性,那么就需要扩展QTreeWidgetItem,当然,C++中扩展修改一个类或组件的方式就是 ...
- VB6史无前例的子类化之透明按钮
[原创文章,转发请保留版权信息] 作者:mezstd 文章地址:http://www.cnblogs.com/imez/p/3299728.html 效果图: 请原谅笔者无耻地称之为史无前例,至少在笔 ...
随机推荐
- DOS批处理中%cd%与%~dp0的区别详解
转载:https://www.jb51.net/article/105325.htm DOS批处理中%cd%与%~dp0的区别详解 Windows下批处理中%cd%和%~dp0都能用来表示当前 ...
- 6-kubernetes网络
1.service存在的意义 防止破的失联(服务发现) 定义一组pod的访问策略(提供负载均衡) 2.pod与service的关系 通过label-selector相关联 通过service实现pod ...
- Centos7系统下Docker开启认证的远程端口2376配置教程
docker开启2375会存在安全漏洞 暴露了2375端口的Docker主机.因为没有任何加密和认证过程,知道了主机IP以后,,任何人都可以管理这台主机上的容器和镜像,以前贪图方便,只开启了没有认证的 ...
- OracleOggan安装并测试同步数据步骤!
Oracle Golden Gate (ogg)安装使用说明 Golden Gate(简称OGG)提供异构环境下交易数据的实时捕捉.变换.投递等功能. OGG支持的异构环境有: OGG的特性: ①对生 ...
- shell-变量的字串应用技术
1. 变量子串的常用操作 常用操作如下表: 依次举例说明: 定义aa变量,内容为"I am scajy" [root@1-241 script]# aa="I am sc ...
- protoc-c 阅读笔记
以前和山哥做过类似的,最近想起来,抽空又看了下 protoc-c. 山哥做的报文流向: rpc -> lydtree -> motree -> struct 涉及的细节很多 1) l ...
- c++的一些习惯
1.显示转换:类型说明符(表达式),如a = int(z) 2.枚举类型enum:专门用来解决对数据的合法性检查问题,如一星期七天,如果用int/char等数据类型描述时,则需要对数据的合法性检查.声 ...
- go-zero 如何应对海量定时/延迟任务?
一个系统中存在着大量的调度任务,同时调度任务存在时间的滞后性,而大量的调度任务如果每一个都使用自己的调度器来管理任务的生命周期的话,浪费cpu的资源而且很低效. 本文来介绍 go-zero 中 延迟操 ...
- swoole热启动
通过扫描指定的要扫描的目录,把所有文件找出来,分别md5 连接字符串,最后再md5返回 启动定时器,扫描,当前的加密值和以前一样不管,否则就重启服务,把当前赋值给旧值 . httpServer.php ...
- Go语言中Goroutine与线程的区别
1.什么是Goroutine? Goroutine是建立在线程之上的轻量级的抽象.它允许我们以非常低的代价在同一个地址空间中并行地执行多个函数或者方法.相比于线程,它的创建和销毁的代价要小很多,并且它 ...