QThread
1. run
函数对于线程的作用相当于main函数对于应用程序。它是线程的入口,run
的开始和结束意味着线程的开始和结束。
2. 事件循环和线程没有必然关系。 QThread
的 run()
方法始终是在一个单独线程执行的,但只有在 run()
函数中使用了 exec()
才真正开启了一个单独的事件循环。
3. 具有时间循环的线程,就可以让QObject
移动到当前线程,具备了使用信号与槽机制与其他线程持续通信的能力.
这里穿插一个概念,所谓线程,不是new了一个线程对象就是线程,这个线程对象其实是在父线程中,跟其它对象一样,new了一个实例而已, 这个实例对象仅仅存在于父线程,但是它可以作为控制新线程的句柄。而真正的线程过程,是run函数启动以后,写在run函数中的代码.
QObject::connect
涉及信号槽,我们就绕不开 connect 函数,只是这个函数大家都比较熟悉。我们只看它的最后一个参数吧(为了简单起见,只看它最常用的3个值):
通过指定connect的连接方式:
1.如果指定直接连接(Direct Connection),则该槽函数将在信号发出的线程中直接执行,而不用判定当前信号发出的线程与槽函数所在线程的状态;
2.如果指定队列连接(Queued Connection),则该槽函数在receiver
所依附的线程中执行;(创建一个QMetaCallEvent
事件,使用QApplication::PostEvent
发送到receiver
所在的线程中,等待receiver
线程的事件循环来处理)
这里顺便提到一点,信号可以在没有事件循环的线程中发送,比如 std::thread 所创建的线程,但是槽函数必须在有事件循环的线程中执行(receiver对象所在的线程必须有事件循环)
3.如果为自动连接(Auto Connection)需要判定发射信号的线程和接受者所依附的线程是否相同, 具体如下:
如果发送信号的线程(`注意不是发送者对象所在的线程`)和接收者所依附的线程相同,则等同于直接连接
如果发射信号的线程和接受者所依附的线程不同,则等同于队列连接
也就是这说,只存在下面两种情况
1.直接连接(Direct Connection)
当信号发射时,槽函数将直接被调用。无论槽函数所属对象在哪个线程,槽函数都在发射信号的线程内执行。
2.队列连接(Queued Connection)
当控制权回到接受者所依附线程的事件循环时,槽函数被调用。槽函数在接收者所依附线程执行。
Qt线程管理的原则:
- QThread 是用来管理线程的,它所依附的线程和它管理的线程并不是同一个东西
- QThread 所依附的线程,就是执行 QThread t 或 QThread * t = new QThread 所在的线程。也就是创建这个线程对象的线程;
- QThread 管理的线程,就是 run 启动的线程。也就是子线程
- 因为QThread的对象依附在主线程中,所以他的slot函数会在主线程中执行,而不是子线程。除非:
- QThread 对象依附到次线程中(通过movetoThread)
- slot 和信号是直接连接(通过connect连接方式来指定),且信号在次线程中发射
QThread 常见的使用方式有两种:
1.继承QThread,重写run()方法:
class WorkerThread : public QThread
{
Q_OBJECT
void run() {
...
}
};
通过WorkerThread对象的start()方法来启动此线程,这将会使run()方法中的函数体在新线程中运行,当run()函数体执行完之后,新线程也就结束了,并会发送出QThread::finished信号;
这种方式与C++的std::thread并无太大差异,都是执行完函数体的内容后,线程自动结束
2.如果我们不希望线程结束,而是希望在有需要的时候,可以在线程中执行其他的动作;那么就需要引入事件循环,需要在QThread的run()方法中调用QThread::exec(), 来启动一个事件循环,这样新线程就不会因为run()函数体的结束而退出了,而是一直在等待事件,可以通过向其发送信号,来使其在新线程中执行指定的动作
class WorkerThread : public QThread
{
Q_OBJECT
void run() {
... // 一些必要的处理
exec(); // 进入事件循环
}
};
如果要结束此线程,可以调用 QThread::quit()和QThread::wait()来使线程退出,具体可以参考:Qt线程安全退出
展开
一. 什么是事件(消息)循环,在下文中,事件 == 消息
事件循环可以理解为一个死循环;试想一下,写一个带有窗口的应用程序,执行了QApplication::exec()函数,那么窗口就不会一闪而过,我们可以与窗口进行交互,窗口可以响应我们的鼠标、键盘等等事,除非我们主动关闭窗口,窗口才会销毁;这是否是一个死循环呢,主动关闭窗口就是退出这个死循环; 事件循环,就是不断从事件队列中获取事件,并响应事件的过程;
二. 事件(消息)队列:
拿Windows系统来说吧,在原生带窗口的Win32应用程序中,只有进程的主线程(即UI线程)
才可以拥有消息队列;或者是调用了GDI函数的线程,操作系统会为其分配一个消息队列
在Qt中,亦是如此,Qt的消息循环也类似于Win32的消息循环,只是Qt将各个平台的原生消息封装为各种QEvent而已;QApplication::exec()执行后,其实只有主线程启动了事件循环并拥有消息队列;如果想要实现Qt的信号槽跨线程通信,那么就需要在其他线程中调用QThread::exec()使其启动事件循环并拥有一个消息队列;
QThread的正确使用方式
三. 在早期,人们使用QThread的两种方法是:
- 不使用事件循环。这是官方的 Manual 、example 以及相关书籍中都介绍的一种的方法。
a. 子类化 QThread
b. 重载 run 函数,run函数内有一个 while 或 for 的死循环
c. 设置一个标记为来控制死循环的退出。
- 使用事件循环。(Qt的开发者Bradley T. Hughes 批驳的就是这种情况下的 一种用法。)
a. 子类化 QThread,
b. 重载 run 使其调用 QThread::exec() ,启动事件循环
c. 并为该类定义信号和槽,这样一来,由于槽函数并不会在新开的 thread 运行,很多人为了解决这个问题在构造函数中调用 moveToThread(this)
而争论和不解正是这样的一条语句造成的。
Bradley T. Hughes 给出说明是: QThread 应该被看做是操作系统线程的接口或控制点,而不应该包含需要在新线程中运行的代码。需要运行的代码应该放到一个QObject的子类中,然后将该子类的对象moveToThread到新线程中。
在Qt4.3(包括)之前,run()函数是纯虚函数,必须子类化QThread来实现run函数。
而从Qt4.4开始, qthreads-no-longer-abstract,run()默认调用 QThread::exec() 启动事件循环。这样一来不需要子类化 QThread 了,只需要子类化一个 QObject 就够了,这正是被 Bradley T. Hughes推荐的方法。
看下面例子:
#include <QtCore/QCoreApplication>
#include <QtCore/QObject>
#include <QtCore/QThread>
#include <QtCore/QDebug>
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=0):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();
}
可以看到结果:run()函数是在子线程中执行的,而slot函数的线程和主线程一致;
main thread: 0x1a40 from thread slot_main: 0x1a40 thread thread: 0x1a48
发送信号的线程 和 接收者所依附的线程。而slot函数属于我们在main中创建的对象 thread,即thread依附于主线程
1.队列连接告诉我们:槽函数在接受者所依附线程执行。即 slot 将在主线程执行
2.直接连接告诉我们:槽函数在发送信号的线程执行。信号在那个线程发送呢??不定!
3.自动连接告诉我们:二者不同,等同于队列连接。即 slot 在主线程执行
假设槽函数中需要执行耗时的操作,会造成主线程阻塞,我们应该怎样处理呢?
根据以上的分析,我们不难想到:
因为QThread的对象依附在主线程中,所以他的slot函数会在主线程中执行,而不是子线程。除非:
- QThread 对象依附到次线程中(通过movetoThread)
- slot 和信号是直接连接,且信号在子线程中发射
第一种方式也就是我们在以上代码中Thread的构造函数中注释的内容,它确实可以达到效果,但是这种方式也正是 Bradley T. Hughes 强烈批判的用法,因为moveToThread不是这样用的
第二种方式则根本达不到效果,因为需要把信号和槽的连接放到Thread类的run函数中去,而Thread对象的创建又是在主线程中,所以线程对象的槽函数一样会在主线程中执行
正确且推荐的方式,moveToThread的正确用法
定义一个普通的QObject派生类,将原来Thread对象的槽函数放到Object对象中去,然后将其对象move到QThread中。使用信号和槽时根本不用考虑多线程的存在。也不用使用QMutex来进行同步,Qt的事件循环会自己自动处理好这个。
#include <QtCore/QCoreApplication>
#include <QtCore/QObject>
#include <QtCore/QThread>
#include <QtCore/QDebug>
class Dummy:public QObject
{
Q_OBJECT
public:
Dummy(QObject* parent=0):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
参考:Qt事件循环与线程