QT中的线程与事件循环理解(2)
1. Qt多线程与Qobject的关系
每一个 Qt 应用程序至少有一个事件循环,就是调用了QCoreApplication::exec()
的那个事件循环。不过,QThread
也可以开启事件循环。只不过这是一个受限于线程内部的事件循环。因此我们将处于调用main()
函数的那个线程,并且由QCoreApplication::exec()
创建开启的那个事件循环成为主事件循环,或者直接叫主循环。注意,QCoreApplication::exec()
只能在调用main()
函数的线程调用。主循环所在的线程就是主线程,也被成为 GUI 线程,因为所有有关 GUI 的操作都必须在这个线程进行。QThread
的局部事件循环则可以通过在QThread::run()
中调用QThread::exec()
开启:
class Thread : public QThread
{
protected:
void run() { }
};
注意:Qt 4.4 版本以后,QThread::run()
不再是纯虚函数,它会调用QThread::exec()
函数。与QCoreApplication
一样,QThread
也有QThread::quit()
和QThread::exit()
函数来终止事件循环。
run 函数是做什么用的?Manual中说的清楚:
run 对于线程的作用相当于main函数对于应用程序。它是线程的入口,run的开始和结束意味着线程的开始和结束。
The run() implementation is for a thread what the main() entry point is for the application. All code executed in a call stack that starts in the run() function is executed by the new thread, and the thread finishes when the function returns.
线程的事件循环用于为线程中的所有QObjects
对象分发事件;默认情况下,这些对象包括线程中创建的所有对象,或者是在别处创建完成后被移动到该线程的对象(我们会在后面详细介绍“移动”这个问题)。我们说,一个QObject
的所依附的线程(thread affinity)是指它所在的那个线程。它同样适用于在QThread
的构造函数中构建的对象:
class MyThread : public QThread
{
public:
MyThread()
{
otherObj = new QObject;
} private:
QObject obj;
QObject *otherObj;
QScopedPointer yetAnotherObj;
};
上面线程对象中的子成员:obj, 以及otherObj所指向的对象,以及yetAnotherObj
,都在使创建Mytherad的线程,即主线程,而不是子线程。
在我们创建了MyThread
对象之后,obj
、otherObj
和yetAnotherObj
的线程依附性是怎样的?是不是就是MyThread
所表示的那个线程?要回答这个问题,我们必须看看究竟是哪个线程创建了它们:实际上,是调用了MyThread
构造函数的线程创建了它们。因此,这些对象不在MyThread
所表示的线程,而是在创建了MyThread
的那个线程中。
(1)QObject::connect
涉及信号槽,我们就躲不过 connect 函数,只是这个函数大家太熟悉。我不好意思再用一堆废话来描述它,但不说又不行,那么折中一下,只看它的最后一个参数吧(为了简单起见,只看它最常用的3个值):
通过指定connect的连接方式,如果指定直接连接(Direct Connection),则该槽函数将再信号发出的线程中直接执行,而不用判定当前信号发出的线程与槽函数所在线程的状态;如果指定队列连接(Queued Connection),则该槽函数在接受者所依附的线程的线程循环中被指定调用;如果为自动连接(Auto Connection)需要判定发射信号的线程和接受者所依附的线程是否相同,进行细分指定。
- 自动连接(Auto Connection)
- 这是默认设置
- 如果发送者的信号(不是发送者对象)在接收者所依附的线程内发射,则等同于直接连接
- 如果发射信号的线程和接受者所依附的线程不同,则等同于队列连接
- 也就是这说,只存在下面两种情况
- 直接连接(Direct Connection)
- 当信号发射时,槽函数将直接被调用。
- 无论槽函数所属对象在哪个线程,槽函数都在发射信号的线程内执行。
- 队列连接(Queued Connection)
- 当控制权回到接受者所依附线程的事件循环时,槽函数被调用。
- 槽函数在接收者所依附线程执行。
(2)qT线程管理的原则:
- QThread 是用来管理线程的,它所依附的线程和它管理的线程并不是同一个东西
- QThread 所依附的线程,就是执行 QThread t 或 QThread * t=new QThread 所在的线程。也就是咱们这儿的主线程
- QThread 管理的线程,就是 run 启动的线程。也就是次线程
- 因为QThread的对象依附在主线程中,所以他的slot函数会在主线程中执行,而不是次线程。除非:
- QThread 对象依附到次线程中(通过movetoThread)
- slot 和信号是直接连接(通过connect连接方式来指定),且信号在次线程中发射
【1】主线程(信号)QThread(槽)
class Dummy:public QObject
{
Q_OBJECT
public:
Dummy(){}
public slots:
void emitsig()
{
emit sig();
}
signals:
void sig();
}; class Thread:public QThread
{
Q_OBJECT
public:
Thread(QObject* parent=):QThread(parent)
{
//moveToThread(this);
}
public slots:
void slot_main()
{
qDebug()<<"from thread slot_main:" <<currentThreadId();
}
protected:
void run()
{
qDebug()<<"thread thread:"<<currentThreadId();
exec();
}
}; #include "main.moc" int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
qDebug()<<"main thread:"<<QThread::currentThreadId();
Thread thread; //槽函数所在的对象依附于线程,
Dummy dummy;
QObject::connect(&dummy, SIGNAL(sig()), &thread, SLOT(slot_main())); //采用默认的链接方式
thread.start();
dummy.emitsig();//信号在主线程中发射
return a.exec();
}
程序运行结果:
main thread: 0x1a40
from thread slot_main: 0x1a40
thread thread: 0x1a48
因为connect采用默认的链接方式,则需要判定发射信号的线程和接受者所依附的线程是否相同,信号在主线程中发射 且槽函数所在的对象依附于线程, 因此链接方式是直接连接,从而运行结果是:槽函数的线程Id和主线程ID是一样的!
因为slot和run处于不同线程,需要线程间的同步!
你会发现 QThread 中 slot 和 run 函数共同操作的对象,都会用QMutex锁住。因为此时run 是另一个线程,即子线程。而slot则是在主线程执行。必须适应锁来保证数据同步。
如果想让槽函数slot在次线程运行(比如它执行耗时的操作,会让主线程卡死)
- 将 thread 依附的线程改为次线程不就行了,这也是代码中注释掉的 moveToThread(this)所做的
去掉注释,你会发现slot在次线程中运行结果:
main thread: 0x13c0
thread thread: 0x1de0
from thread slot_main: 0x1de0
但这是 Bradley T. Hughes 强烈批判的用法。不推荐这样使用。
【2】run中信号与QThread中槽
即,信号在子线程发射,而槽函数谁线程的槽函数,而线程对象在主线程中创建,如果链接采用自动链接,则条件判断比为队列连接,且由主线程在主线程的事件循环中执行。如下所示:
class Dummy:public QObject
{
Q_OBJECT
public:
Dummy(QObject* parent=):QObject(parent){}
public slots:
void emitsig()
{
emit sig();
}
signals:
void sig();
};
class Thread:public QThread
{
Q_OBJECT
public:
Thread(QObject* parent=):QThread(parent)
{
//moveToThread(this);
}
public slots:
void slot_thread()
{
qDebug()<<"from thread slot_thread:" <<currentThreadId();
}
signals:
void sig();
protected:
void run()
{
qDebug()<<"thread thread:"<<currentThreadId();
Dummy dummy;
connect(&dummy, SIGNAL(sig()), this, SLOT(slot_thread()));
dummy.emitsig();
exec();
}
};
#include "main.moc"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
qDebug()<<"main thread:"<<QThread::currentThreadId();
Thread thread;
thread.start();
return a.exec();
}
运行结果:槽函数在主线程中执行。
main thread: 0x15c0
thread thread: 0x1750
from thread slot_thread: 0x15c0
如果指定为直接连接方式,则槽函数将在次线程(信号发出的线程)执行,这样,你需要处理slot和它的对象所在线程的同步。需要 QMutex 一类的东西。
推荐的方法
其实,这个方法太简单,太好用了。定义一个普通的QObject派生类,然后将其对象move到QThread中。使用信号和槽时根本不用考虑多线程的存在。也不用使用QMutex来进行同步,Qt的事件循环会自己自动处理好这个。
class Dummy:public QObject
{
Q_OBJECT
public:
Dummy(QObject* parent=):QObject(parent) {}
public slots:
void emitsig()
{
emit sig();
}
signals:
void sig();
}; class Object:public QObject
{
Q_OBJECT
public:
Object(){}
public slots:
void slot()
{
qDebug()<<"from thread slot:" <<QThread::currentThreadId();
}
}; #include "main.moc" int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
qDebug()<<"main thread:"<<QThread::currentThreadId();
QThread thread;
Object obj;
Dummy dummy;
obj.moveToThread(&thread); // 必须在对象的依附线程中执行此函数
QObject::connect(&dummy, SIGNAL(sig()), &obj, SLOT(slot()));
thread.start();
dummy.emitsig();
return a.exec();
}
执行结果:
main thread: 0x1a5c
from thread slot: 0x186c
确实简单,只需要再object的子类中新建“耗时功能”的实现“即可,然后将此对象moveToThread 到线程对象即可。
2. QT多线程原则
我们可以通过调用QObject::thread()
可以查询一个QObject
的线程依附性。
注意,在QCoreApplication
对象之前创建的QObject
没有所谓线程依附性,因此也就没有对象为其派发事件。也就是说,实际是QCoreApplication
创建了代表主线程的QThread
对象。
我们可以使用线程安全的QCoreApplication::postEvent()
函数向一个对象发送事件。它将把事件加入到对象所在的线程的事件队列中,因此,如果这个线程没有运行事件循环,即没有依附的线程,这个事件也不会被派发。但是可以通过将这种浮游对象通过QObject::moveToThread()
来移入到一个已有的线程中,从而确保这些浮游的对象可以依附线程。
值得注意的一点是,虽然QObject
是可重入的,但是 GUI 类,特别是QWidget
及其所有的子类,都是不是可重入的。它们只能在主线程使用。由于这些 GUI 类大都需要一个事件循环,所以,调用QCoreApplication::exec()
也必须是主线程,否则这些 GUI 类就没有事件循环了。你不能有两个线程同时访问一个QObject
对象,除非这个对象的内部数据都已经很好地序列化(例如为每个数据访问加锁)。记住,在你从另外的线程访问一个对象时,它可能正在处理所在线程的事件循环派发的事件!基于同样的原因,你也不能在另外的线程直接delete
一个QObject
对象,相反,你需要调用QObject::deleteLater()
函数,这个函数会给对象所在线程发送一个删除的事件。
(1)QObject
的线程依附性是可以改变的,方法是调用QObject::moveToThread()
函数。该函数会改变一个对象及其所有子对象的线程依附性。由于QObject
不是线程安全的,所以我们只能在该对象所在线程上调用这个函数。也就是说,我们只能在对象所在线程将这个对象移动到另外的线程,不能在另外的线程改变对象的线程依附性。
(2)Qt 要求QObject
的所有子对象都必须和其父对象在同一线程。这意味着:
- 不能对有父对象(parent 属性)的对象使用
QObject::moveToThread()
函数 - 不能在
QThread
中以这个QThread
本身作为父对象创建对象,,这是因为要创建该线程对象必然在其他的线程中创建,即该线程对象必然依附于其他线程对象,而以该线程对象为父类的子对向,在run函数中进行新建子类对象,若以其作为父对象,则与QT所定义的原则冲突,因此禁止。
class Thread : public QThread {
void run() {
QObject *obj = new QObject(this); // 错误!
}
};
这是因为QThread
对象所依附的线程是创建它的那个线程,而不是它所代表的线程。
(3)Qt 还要求,在代表一个线程的QThread
对象销毁之前,所有在这个线程中的对象都必须先delete
。
要达到这一点并不困难:我们只需在QThread::run()
的栈空间(直接定义对象)上创建对象即可。
现在的问题是,既然线程创建的对象都只能在函数栈上,怎么能让这些对象与其它线程的对象通信呢?Qt 提供了一个优雅清晰的解决方案:我们在线程的事件队列中加入一个事件,然后在事件处理函数中调用我们所关心的函数。显然这需要线程有一个事件循环。这种机制依赖于 moc 提供的反射:因此,只有信号、槽和使用Q_INVOKABLE
宏标记的函数可以在另外的线程中调用。
QMetaObject::invokeMethod()
静态函数会这样调用:
QMetaObject::invokeMethod(object, "methodName",
Qt::QueuedConnection,
Q_ARG(type1, arg1),
Q_ARG(type2, arg2));
上面函数调用中出现的参数类型都必须提供一个公有构造函数,一个公有的析构函数和一个公有的复制构造函数,并且要使用qRegisterMetaType()
函数向 Qt 类型系统注册。
跨线程的信号槽也是类似的。当我们将信号与槽连接起来时,QObject::connect()
的最后一个参数将指定连接类型:
Qt::DirectConnection
:直接连接意味着槽函数将在信号发出的线程直接调用Qt::QueuedConnection
:队列连接意味着向接受者所在线程发送一个事件,该线程的事件循环将获得这个事件,然后之后的某个时刻调用槽函数Qt::BlockingQueuedConnection
:阻塞的队列连接就像队列连接,但是发送者线程将会阻塞,直到接受者所在线程的事件循环获得这个事件,槽函数被调用之后,函数才会返回Qt::AutoConnection
:自动连接(默认)意味着如果接受者所在线程就是当前线程,则使用直接连接;否则将使用队列连接;即如果接受者依附的线程就是当前线程,则直接连接,信号发出就调用。如果接受者依附的线程是其他线程,则队列连接,即向接受者所在线程发送一个事件,该线程的事件循环将获得这个事件,然后之后的某个时刻调用槽函数。
注意在上面每种情况中,发送者所在线程都是无关紧要的!在自动连接情况下,Qt 需要查看信号发出的线程是不是与接受者所在线程一致,来决定连接类型。注意,Qt 检查的是信号发出的线程,而不是信号发出的对象所在的线程!我们可以看看下面的代码:
class Thread : public QThread
{
Q_OBJECT
signals:
void aSignal();
protected:
void run() {
emit aSignal();
}
}; /* ... */
Thread thread;
Object obj;
QObject::connect(&thread, SIGNAL(aSignal()), &obj, SLOT(aSlot()));
thread.start();
aSignal()
信号在一个新的线程被发出(也就是Thread
所代表的线程)。注意,因为这个线程并不是Object
所在的线程(Object
所在的线程和Thread
所在的是同一个线程),但是aSignal()
确实在Thread
所代表的新线程中发出,因此,必然是队列连接。
3. 多线程的数据交互,数据同步问题
线程对象依附的线程VS线程代表的新线程执行中对原有线程的访问问题
class Thread : public QThread
{
Q_OBJECT
slots:
void aSlot() {
/* ... */
}
protected:
void run() {
/* ... */
}
}; /* ... */
Thread thread;
Object obj;
QObject::connect(&obj, SIGNAL(aSignal()), &thread, SLOT(aSlot()));
thread.start();
obj.emitSignal();
这里的obj
发出aSignal()
信号时,使用哪种连接方式?答案是:直接连接。因为Thread
对象所在线程发出了信号,也就是信号发出的线程与接受者是同一个。在aSlot()
槽函数中,我们可以直接访问Thread
的某些成员变量,但是注意,在我们访问这些成员变量时,Thread::run()
函数可能也在访问!这意味着二者并发进行:这是一个完美的导致崩溃的隐藏bug
class Thread : public QThread
{
Q_OBJECT
slots:
void aSlot() {
/* ... */
}
protected:
void run() {
QObject *obj = new Object;
connect(obj, SIGNAL(aSignal()), this, SLOT(aSlot()));
/* ... */
}
};
上面也是队列连接,且Thread的aSlot槽函数依附于Thread对象的依附线程,即主线程。因此与子线程中的信号不在同一个线程中。
如果为了在子线程中调用线程对象本身的槽函数,且槽函数的执行也在该子线程中。则采用
1. 在线程的构造函数中使用QObject::moveToThread()
方法2. 直接指定连接方式为 :Driect连接方式
3. 采用上文中推荐的方法。
第一种:在线程构造函数中,QThread
对象不是线程本身,将改对象依附到其自己所创建的线程中。
实际上,这的确可行(因为Thread
的线程依附性被改变了:它所在的线程成了自己),但是这并不是一个好主意。这种代码意味着我们其实误解了线程对象(QThread
子类)的设计意图:它们其实是用于管理它所代表的线程的对象。因此,它们应该在另外的线程被使用(通常就是它自己所在的线程),而不是在自己所代表的线程中。
class Thread : public QThread {
Q_OBJECT
public:
Thread() {
moveToThread(this); // 错误!,不推荐
} /* ... */
};
第二种:也不推荐
第三种:最好的解决方式,就是采用上面提到的,我们可以利用一个QObject
的子类,使用QObject::moveToThread()
改变其线程依附性:将处理任务的部分与管理线程的部分分离。
endl;
QT中的线程与事件循环理解(2)的更多相关文章
- QT中的线程与事件循环理解(1)
1.需要使用多线程管理的例子 一个进程可以有一个或更多线程同时运行.线程可以看做是“轻量级进程”,进程完全由操作系统管理,线程即可以由操作系统管理,也可以由应用程序管理.Qt 使用QThread 来管 ...
- Qt 学习之路 2(72):线程和事件循环
Qt 学习之路 2(72):线程和事件循环 <理解不清晰,不透彻> -- 有需求的话还需要进行专题学习 豆子 2013年11月24日 Qt 学习之路 2 34条评论 前面一章我 ...
- Qt 学习之路:线程和事件循环
前面一章我们简单介绍了如何使用QThread实现线程.现在我们开始详细介绍如何“正确”编写多线程程序.我们这里的大部分内容来自于Qt的一篇Wiki文档,有兴趣的童鞋可以去看原文. 在介绍在以前,我们要 ...
- Qt 的线程与事件循环——可打印threadid进行观察槽函数到底是在哪个线程里执行,学习moveToThread的使用)
周末天冷,索性把电脑抱到床上上网,这几天看了 dbzhang800 博客关于 Qt 事件循环的几篇 Blog,发现自己对 Qt 的事件循环有不少误解.从来只看到现象,这次借 dbzhang800 的博 ...
- Qt 的线程与事件循环
Qt 的线程与事件循环
- Qt中暂停线程的执行(主线程和工作线程共用一把锁,一旦主线程将它锁上,工作线程就无法运行了,这也是一个办法)
在线程中定义一个信号量: QMutex pause; 把run()函数中循环执行的部分用信号量pause锁住: void run() { while(1) { pause.lock ...
- 如何给循环中的对象添加事件--深入理解JavaScript的闭包特性
初学者经常碰到的,即获取HTML元素集合,循环给元素添加事件.在事件响应函数中(event handler)获取对应的索引.但每次获取的都是最后一次循环的索引.原因是初学者并未理解JavaScript ...
- node.js中对Event Loop事件循环的理解
javascript是单线程的,所以任务的执行都需要排队,任务分为两种,一种是同步任务,一种是异步任务. 同步任务是进入主线程上排队执行的任务,上一个任务执行完了,下一个任务才会执行. 异步任务是不进 ...
- Qt入门(9)——Qt中的线程支持
Qt对线程提供了支持,基本形式有独立于平台的线程类.线程安全方式的事件传递和一个全局Qt库互斥量允许你可以从不同的线程调用Qt方法.警告:所有的GUI类(比如,QWidget和它的子类),操作系统核心 ...
随机推荐
- JAVA动手动脑及课后作业
1.查看其输出结果.如何解释这样的输出结果?从中你能总结出什么? 运行结果 true true false 原因 1)在Java中,内容相同的字串常量(“Hello”)只保存一份以节约内存,所以s0, ...
- 7.Mysql存储引擎
7.表类型(存储引擎)的选择7.1 Mysql存储引擎概述 mysql支持插件式存储引擎,即存储引擎以插件形式存在于mysql库中. mysql支持的存储引擎包括:MyISAM.InnoDB.BDB. ...
- python添加fluent日志记录
istio默认会进行日志的记录,但是仅仅记录到服务.以及服务之间调用的信息,不记录业务日志. 如: 所以需要添加业务日志记录. 1.python引入package fluentmsgpack 2.代码 ...
- Python 字符串(center)
center 描述 Python center() 返回一个原字符串居中,并使用空格填充至长度 width 的新字符串.默认填充字符为空格. 语法 center()方法语法: str.center(w ...
- UI和View 三种控制方式
AndroidManifest.xml <?xml version="1.0" encoding="utf-8"?> <manifest xm ...
- C&Cpp.CallGraph
1. CodeViz http://www.skynet.ie/~mel/projects/codeviz/ 2. http://my.oschina.net/zmlblog/blog/186308
- Jenkins2.0中的pipeline
jenkins的实现是标准的master/slave模式,用户与master交互,master将job分布到slave上运行. jenkins的基本概念: 1. master, 也就是jenkins的 ...
- Xstream将XML转换为javabean的问题
1.问题:Xstream is not security 解决方法:加上 2.问题:如果没有第二行代码,会出现xstream forbiddenclassexception 解决方法:加上第二行,其中 ...
- 基因组表达分析:如何选择RNA-seq vs. 芯片
基因组表达分析:如何选择RNA-seq vs. 芯片 发布日期:2017-03-29 10:00 DNA 芯片(上图左侧)由附着在表面的核酸探针组成.首先,从样品中提取 RNA 并转化为互补 DNA( ...
- qrcode解决方案大全
QRCODE解决方案 1.delphi原生QRCode.pas 2.delphi编写http服务器实现QRcode请求生成图片,http下载 3.delphi编写cgi程序,加载到apache服务器 ...