1.子线程中创建的对象不应再其他线程中被调用,包括使用槽函数的形式
在创建了MyThread 对象后,obj, otherObj, yetAnotherObj 的线程依附性是怎么样的?要回答这个问题,我们必须要看一下创建他们的线程:是这个运行MyThread 构造函数的线程创建了他们。因此,这三个对象并没有驻足在MyThread 线程,而是驻足在创建MyThread 实例的线程中。
要注意的是在QCoreApplication 对象之前创建的QObjects没有依附于某一个线程。因此,没有人会为它们做事件派发处理。(换句话说,QCoreApplication 构建了代表主线程的QThread 对象)
我们可以使用线程安全的QCoreApplication::postEvent() 方法来为某个对象分发事件。它将把事件加入到对象所驻足的线程事件队列中。因此,除非事件对象依附的线程有一个正在运行的事件循环,否则事件不会被派发。
理解QObject和它所有的子类不是线程安全的(尽管是可重入的)非常重要;因此,除非你序列化对象内部数据 所有可访问的接口、数据,否则你不能让多个线程同一时刻访问相同的QObject(比如,用一个锁来保护)。请注意,尽管你可以从另一个线程访问对象,但 是该对象此时可能正在处理它所驻足的线程事件循环派发给它的事件! 基于这种原因,你不能从另一个线程去删除一个QObject,一定要使用QObject::deleteLater(),它会Post一个事件,目标删除 对象最终会在它所生存的线程中被删除。(译者注:QObject::deleteLater作用是,当控制流回到该对象所依附的线程事件循环时,该对象才会被“本”线程中删除)。
此外,QWidget 和它所有的子类,以及所有与GUI相关的类(即便不是基于QObject的,像QPixmap)并不是可重入的。它们必须专属于GUI线程。
我们可以通过调用QObject::moveToThread()来改变一个QObject的依附性;它将改变这个对象以及它的孩子们的依附性。因为QObject不是线程安全的,我们必须在对象所驻足的线程中使用此函数;也就是说,你只能将对象从它所驻足的线程中推送到其他线程中,而不能从其他线程中拉回来。此外,Qt要求一个QObject的孩子必须与它们的双亲驻足在同一个线程中。这意味着:
你不能使用QObject::moveToThread()作用于有双亲的对象;
你千万不要在一个线程中创建对象的同时把QThread对象自己作为它的双亲。 (注:两者不在同一个线程中):
run函数中的代码时确定无疑要在次线程中运行的,那么其他的呢?比如 slot 是在次线程还是主线程中运行?
QObject::connect() 涉及信号槽,我们就躲不过 connect 函数,它的第五个参数是设置连接类型的。(为了简单起见,只看它最常用的3个值)
自动连接(Auto Connection),这是默认设置,如果信号在接收者所依附的线程内发射,则等同于直接连接,如果发射信号的线程和接受者所依附的线程不同,则等同于队列连接。
直接连接(Direct Connection),当信号发射时,槽函数将直接被调用。无论槽函数所属对象在哪个线程,槽函数都在发射信号的线程内执行。
队列连接(Queued Connection),当控制权回到接受者所依附线程的事件循环时,槽函数被调用。槽函数在接收者所依附线程执行。
连接类型强调两个概念:发送信号的线程 和 接收者所依附的线程。而 slot 函数属于我们在main中创建的对象 thread,即thread依附于主线程。
队列连接告诉我们:槽函数在接受者所依附线程执行。即 slot 将在主线程执行。
直接连接告诉我们:槽函数在发送信号的线程执行。信号在那个线程发送呢?不定!
自动连接告诉我们:二者不同,等同于队列连接。即 slot 在主线程执行。
太绕了?不是么(要彻底理解这几句话,你可能需要看Qt meta-object系统和Qt event系统)。
怎么理解到底在哪个线程执行slot()函数?
需要搞清楚QThread的这个类。
QThread 是用来管理线程的,它所依附的线程和它管理的线程并不是同一个东西。
QThread 所依附的线程,就是执行 QThread t(0) 或 QThread * t=new QThread(0) 的线程。也就是咱们这儿的主线程。
QThread 管理的线程,就是 run 启动的线程。也就是次线程。
因为QThread的对象依附在主线程中,所以他的slot函数会在主线程中执行,而不是次线程。
除非:QThread 对象依附到次线程中(通过movetoThread)。
slot 和信号是直接连接,且信号在次线程中发射。
但上两种解决方法都不好,因为QThread不是这么用的(Bradley T. Hughes)
通过代码的方式应该比较好理解
主线程(信号)QThread(槽), 但由于没说槽函数是在主线程执行的,所以不少人都认为它应该是在次线程执行了。
前面两个章节我们从事件循环和线程类库两个角度阐述有关线程的问题。本章我们将深入线程间得交互,探讨线程和QObject
之间的关系。在某种程度上,这才是多线程编程真正需要注意的问题。
现在我们已经讨论过事件循环。我们说,每一个 Qt 应用程序至少有一个事件循环,就是调用了QCoreApplication::exec()
的那个事件循环。不过,QThread
也可以开启事件循环。只不过这是一个受限于线程内部的事件循环。因此我们将处于调用main()
函数的那个线程,并且由QCoreApplication::exec()
创建开启的那个事件循环成为主事件循环,或者直接叫主循环。注意,QCoreApplication::exec()
只能在调用main()
函数的线程调用。主循环所在的线程就是主线程,也被成为 GUI 线程,因为所有有关 GUI 的操作都必须在这个线程进行。QThread
的局部事件循环则可以通过在QThread::run()
中调用QThread::exec()
开启:
C++
1 2 3 4 5 6 7 8 | class Thread : public QThread { protected: void run() { /* ... 初始化 ... */ exec(); } }; |
记得我们前面介绍过,Qt 4.4 版本以后,QThread::run()
不再是纯虚函数,它会调用QThread::exec()
函数。与QCoreApplication
一样,QThread
也有QThread::quit()
和QThread::exit()
函数来终止事件循环。
线程的事件循环用于为线程中的所有QObjects
对象分发事件;默认情况下,这些对象包括线程中创建的所有对象,或者是在别处创建完成后被移动到该线程的对象(我们会在后面详细介绍“移动”这个问题)。我们说,一个QObject
的所依附的线程(thread affinity)是指它所在的那个线程。它同样适用于在QThread
的构造函数中构建的对象:
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 | class MyThread : public QThread { public: MyThread() { otherObj = new QObject; } private: QObject obj; QObject *otherObj; QScopedPointer yetAnotherObj; }; |
在我们创建了MyThread
对象之后,obj
、otherObj
和yetAnotherObj
的线程依附性是怎样的?是不是就是MyThread
所表示的那个线程?要回答这个问题,我们必须看看究竟是哪个线程创建了它们:实际上,是调用了MyThread
构造函数的线程创建了它们。因此,这些对象不在MyThread
所表示的线程,而是在创建了MyThread
的那个线程中。
我们可以通过调用QObject::thread()
可以查询一个QObject
的线程依附性。注意,在QCoreApplication
对象之前创建的QObject
没有所谓线程依附性,因此也就没有对象为其派发事件。也就是说,实际是QCoreApplication
创建了代表主线程的QThread
对象。
我们可以使用线程安全的QCoreApplication::postEvent()
函数向一个对象发送事件。它将把事件加入到对象所在的线程的事件队列中,因此,如果这个线程没有运行事件循环,这个事件也不会被派发。
值得注意的一点是,QObject
及其所有子类都不是线程安全的(但都是可重入的)。因此,你不能有两个线程同时访问一个QObject
对象,除非这个对象的内部数据都已经很好地序列化(例如为每个数据访问加锁)。记住,在你从另外的线程访问一个对象时,它可能正在处理所在线程的事件循环派发的事件!基于同样的原因,你也不能在另外的线程直接delete
一个QObject
对象,相反,你需要调用QObject::deleteLater()
函数,这个函数会给对象所在线程发送一个删除的事件。
此外,QWidget
及其子类,以及所有其它 GUI 相关类(即便不是QObject
的子类,例如QPixmap
),甚至不是可重入的:它们只能在 GUI 线程访问。
QObject
的线程依附性是可以改变的,方法是调用QObject::moveToThread()
函数。该函数会改变一个对象及其所有子对象的线程依附性。由于QObject
不是线程安全的,所以我们只能在该对象所在线程上调用这个函数。也就是说,我们只能在对象所在线程将这个对象移动到另外的线程,不能在另外的线程改变对象的线程依附性。还有一点是,Qt 要求QObject
的所有子对象都必须和其父对象在同一线程。这意味着:
- 不能对有父对象(parent 属性)的对象使用
QObject::moveToThread()
函数 - 不能在
QThread
中以这个QThread
本身作为父对象创建对象,例如: C++1
2
3
4
5
class Thread : public QThread {
void run() {
QObject *obj = new QObject(this); // 错误!
}
};
这是因为QThread
对象所依附的线程是创建它的那个线程,而不是它所代表的线程。
Qt 还要求,在代表一个线程的QThread
对象销毁之前,所有在这个线程中的对象都必须先delete
。要达到这一点并不困难:我们只需在QThread::run()
的栈上创建对象即可。
现在的问题是,既然线程创建的对象都只能在函数栈上,怎么能让这些对象与其它线程的对象通信呢?Qt 提供了一个优雅清晰的解决方案:我们在线程的事件队列中加入一个事件,然后在事件处理函数中调用我们所关心的函数。显然这需要线程有一个事件循环。这种机制依赖于 moc 提供的反射:因此,只有信号、槽和使用Q_INVOKABLE
宏标记的函数可以在另外的线程中调用。
QMetaObject::invokeMethod()
静态函数会这样调用:
C++
1 2 3 4 | 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 检查的是信号发出的线程,而不是信号发出的对象所在的线程!我们可以看看下面的代码:
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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
所在的是同一个线程,回忆下,信号槽的连接方式与发送者所在线程无关),所以这里将会使用队列连接。
另外一个常见的错误是:
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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。
另外一个例子可能更为重要:
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Thread : public QThread { Q_OBJECT slots: void aSlot() { /* ... */ } protected: void run() { QObject *obj = new Object; connect(obj, SIGNAL(aSignal()), this, SLOT(aSlot())); /* ... */ } }; |
这个例子也会使用队列连接。然而,这个例子比上面的例子更具隐蔽性:在这个例子中,你可能会觉得,Object
所在Thread
所代表的线程中被创建,又是访问的Thread
自己的成员数据。稍有不慎便会写出这种代码。
为了解决这个问题,我们可以这么做:Thread
构造函数中增加一个函数调用:moveToThread(this)
:
C++
1 2 3 4 5 6 7 8 9 | class Thread : public QThread { Q_OBJECT public: Thread() { moveToThread(this); // 错误! } /* ... */ }; |
实际上,这的确可行(因为Thread
的线程依附性被改变了:它所在的线程成了自己),但是这并不是一个好主意。这种代码意味着我们其实误解了线程对象(QThread
子类)的设计意图:QThread
对象不是线程本身,它们其实是用于管理它所代表的线程的对象。因此,它们应该在另外的线程被使用(通常就是它自己所在的线程),而不是在自己所代表的线程中。
上面问题的最好的解决方案是,将处理任务的部分与管理线程的部分分离。简单来说,我们可以利用一个QObject
的子类,使用QObject::moveToThread()
改变其线程依附性:
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class Worker : public QObject { Q_OBJECT public slots: void doWork() { /* ... */ } }; /* ... */ QThread *thread = new QThread; Worker *worker = new Worker; connect(obj, SIGNAL(workReady()), worker, SLOT(doWork())); worker->moveToThread(thread); thread->start(); |
/*******************************************************
1.QThread中的slots是在那个线程中执行?
QThread::run() 函数的作用:run 对于线程的作用相当于main函数对于应用程序。它是线程的入口,run的开始和结束意味着线程的开始和结束。
class Thread:public QThread
{
Q_OBJECT
public:
Thread(QObject* parent=0):QThread(parent){}
public slots:
void slot() { ... } //slot函数依附于主线程
signals:
void sig();
protected:
void run() { ... } //run函数里的代码在次线程中执行
};
int main(int argc, char** argv)
{
...
Thread thread;
...
}
run函数中的代码时确定无疑要在次线程中运行的,那么其他的呢?比如 slot 是在次线程还是主线程中运行?
QObject::connect() 涉及信号槽,我们就躲不过 connect 函数,它的第五个参数是设置连接类型的。(为了简单起见,只看它最常用的3个值)
自动连接(Auto Connection),这是默认设置,如果信号在接收者所依附的线程内发射,则等同于直接连接,如果发射信号的线程和接受者所依附的线程不同,则等同于队列连接。
直接连接(Direct Connection),当信号发射时,槽函数将直接被调用。无论槽函数所属对象在哪个线程,槽函数都在发射信号的线程内执行。
队列连接(Queued Connection),当控制权回到接受者所依附线程的事件循环时,槽函数被调用。槽函数在接收者所依附线程执行。
连接类型强调两个概念:发送信号的线程 和 接收者所依附的线程。而 slot 函数属于我们在main中创建的对象 thread,即thread依附于主线程。
队列连接告诉我们:槽函数在接受者所依附线程执行。即 slot 将在主线程执行。
直接连接告诉我们:槽函数在发送信号的线程执行。信号在那个线程发送呢?不定!
自动连接告诉我们:二者不同,等同于队列连接。即 slot 在主线程执行。
太绕了?不是么(要彻底理解这几句话,你可能需要看Qt meta-object系统和Qt event系统)。
怎么理解到底在哪个线程执行slot()函数?
需要搞清楚QThread的这个类。
QThread 是用来管理线程的,它所依附的线程和它管理的线程并不是同一个东西。
QThread 所依附的线程,就是执行 QThread t(0) 或 QThread * t=new QThread(0) 的线程。也就是咱们这儿的主线程。
QThread 管理的线程,就是 run 启动的线程。也就是次线程。
因为QThread的对象依附在主线程中,所以他的slot函数会在主线程中执行,而不是次线程。
除非:QThread 对象依附到次线程中(通过movetoThread)。
slot 和信号是直接连接,且信号在次线程中发射。
但上两种解决方法都不好,因为QThread不是这么用的(Bradley T. Hughes)
通过代码的方式应该比较好理解
主线程(信号)QThread(槽), 但由于没说槽函数是在主线程执行的,所以不少人都认为它应该是在次线程执行了。
定义一个 Dummy 类,用来发信号。
定义一个 Thread 类,用来接收信号,重载 run 函数,目的是打印 threadid。
#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;
//主线程的sig信号,thread依附在主线程中的slot_main槽
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
可以看到,主线程id和slot_main()中的线程id是一样的,槽函数的线程和主线程是一样的!所以thread类中的slot_main()是在主线程执行,run()函数是在次线程执行。
如果你看过Qt自带的例子,你会发现 QThread 中 slot 和 run 函数共同操作的对象,都会用QMutex锁住。为什么?因为slot和run处于不同线程,需要线程间的同步!
如果想让槽函数slot在次线程运行(比如它执行耗时的操作,会让主线程死掉),怎么解决呢?(就是想让这个slot_main()槽函数去次线程中执行,虽然它是依附于主线程的)
参考我们前面的结论,很容易想到:将 thread 依附的线程改为次线程不就行了?也是代码中注释掉的 moveToThread(this)所做的就是将整个 thread 类移到次线程中,去掉注释,你会发现slot在次线程中运行。打印输出结果:
main thread: 0x13c0
thread thread: 0x1de0
from thread slot_main: 0x1de0
此时的 slot_main() 槽函数就是在次线程中执行了。
但是 Bradley T. Hughes作者强烈批判的用法,觉得这样写不规范。推荐的方法后面会给出。
run中信号与QThread中槽
定义一个 Dummy 类,在run中发射它的信号,也可以在run中发射 Thread 类中的信号,而不是Dummy(效果完全一样)。QThread 定义槽函数,重载run函数。
#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 Thread:public QThread
{
Q_OBJECT
public:
Thread(QObject* parent=0):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;
//次线程中的sig(),主线程中的slot_main()
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_main() 肯定是在主线程执行。thread 对象本身在主线程。所以它的槽也在要在主线程执行。
如何让slot_main() 在子线程中执行呢?
(方法一)前面提了 moveToThread,这儿可以用,而且可以解决问题。当同样,是被批判的对象。
(方法二)注意哦,这儿我们的信号时次线程发出的,对比connect连接方式,会发现:采用直接连接,槽函数将在次线程(信号发出的线程)执行。这个方法不太好,因为你需要处理slot和它的对象所在线程的同步。需要 QMutex 一类的东西。
推荐的方法:
其实,这个方法太简单,太好用了。
定义一个普通的QObject派生类,然后将其对象 moveToThread() 到 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); //是将obj这个对象移入次线程,而不是讲QThread移入
//次线程中的sig(),次线程中的slot()
QObject::connect(&dummy, SIGNAL(sig()), &obj, SLOT(slot()));
thread.start();
dummy.emitsig();
return a.exec();
}
打印输出结果:
main thread: 0x1a5c
from thread slot: 0x186c
slot() 确实不在主线程中运行了。
其他:
本文只考虑了使用事件循环的情况,也有可能run中没有事件循环。这时信号与槽会与本文有点差别。比如run中使用connect时,队列连接就受限制了。其实只要理解了前面这些,没有事件循环的情况很容易就想通了。
/*************************************************************
Qt 线程类
Qt 4.0版本的release则对其所有所支持平台默认地是对多线程支持的。(当然你也可以关掉对线程的支持,参见这里)。现在Qt提供了不少类用于处理线程,让你我们首先预览一下:
QThread 是Qt中一个对线程支持的核心的底层类。 每个线程对象代表了一个运行的线程。由于Qt的跨平台特性,QThread成功隐藏了所有在不同操作系统里使用线程的平台相关性代码。
为了运用QThread从而让代码在一个线程里运行,我们可以创建一个QThread的子类,并重载QThread::run() 方法:
- class Thread : public QThread {
- protected:
- void run() {
- /* your thread implementation goes here */
- }
- };
接着,我们可以使用:
- class Thread : public QThread {
- protected:
- void run() {
- /* your thread implementation goes here */
- }
- };
来真正的启动一个新的线程。 请注意,Qt 4.4版本之后,QThread不再支持抽象类;现在虚函数QThread::run()实际上是简单调用了QThread::exec(),而它启动了线程的事件循环。(更多信息见后文)
QRunnable [doc.qt.nokia.com] 是一种轻量级的、以“run and forget”方式来在另一个线程开启任务的抽象类,为了实现这一功能,我们所需要做的全部事情是派生QRunnable 类,并实现纯虚函数方法run()
- class Task : public QRunnable {
- public:
- void run() {
- /* your runnable implementation goes here */
- }
- };
事实上,我们是使用QThreadPool 类来运行一个QRunnable 对象,它维护了一个线程池。通过调用QThreadPool::start(runnable) ,我们把一个QRunnable 放入了QThreadPool的运行队列中;只要线程是可见得,QRunnable 将会被拾起并且在那个线程里运行。尽管所有的Qt应用程序都有一个全局的线程池,且它是通过调用 QThreadPool::globalInstance()可见得,但我们总是显式地创建并管理一个私有的QThreadPool 实例。
请注意,QRunnable 并不是一个QObject类,它并没有一个内置的与其他组件显式通讯的方法。你必须使用底层的线程原语(比如收集结构的枷锁保护队列等)来亲自编写代码。
QtConcurrent 是一个构建在QThreadPool之上的上层API,它用于处理最普通的并行计算模式:map [en.wikipedia.org], reduce [en.wikipedia.org], and filter [en.wikipedia.org] 。同时,QtConcurrent::run()方法提供了一种便于在另一个线程运行一个函数的方法。
不像QThread 以及QRunnable,QtConcurrent 没有要求我们使用底层的同步原语,QtConcurrent 所有的方法会返回一个QFuture 对象,它包含了结果而且可以用来查询线程计算的状态(它的进度),从而暂停、继续、取消计算。QFutureWatcher 可以用来监听一个QFuture 进度,并且通过信号和槽与之交互(注意QFuture是一个基于数值的类,它并没有继承自QObject).
/ | QThread | QRunnable | QtConcurrent1 |
---|---|---|---|
High level API | ✘ | ✘ | ✔ |
Job-oriented | ✘ | ✔ | ✔ |
Builtin support for pause/resume/cancel | ✘ | ✘ | ✔ |
Can run at a different priority | ✔ | ✘ | ✘ |
Can run an event loop | ✔ | ✘ | ✘ |
线程与QObjects
我们在上文中已经讨论了事件循环,我们可能理所当然地认为在Qt的应用程序中只有一个事件循环,但事实并不是这样:QThread对象在它们所代表的线程中开启了新的事件循环。因此,我们说main 事件循环是由调用main()的线程通过QCoreApplication::exec() 创建的。 它也被称做是GUI线程,因为它是界面相关操作唯一允许的进程。一个QThread的局部事件循环可以通过调用QThread::exec() 来开启(它包含在run()方法的内部)
- class Thread : public QThread {
- protected:
- void run() {
- /* ... initialize ... */
- exec();
- }
- };
正如我们之前所提到的,自从Qt 4.4 的QThread::run() 方法不再是一个纯虚函数,它调用了QThread::exec()。就像QCoreApplication,QThread 也有QThread::quit() 和QThread::exit()来停止事件循环。
一个线程的事件循环为驻足在该线程中的所有QObjects派发了所有事件,其中包括在这个线程中创建的所有对象,或是移植到这个线程中的对象。我们说一个QObject的线程依附性(thread affinity)是指某一个线程,该对象驻足在该线程内。我们在任何时间都可以通过调用QObject::thread()来查询线程依附性,它适用于在QThread对象构造函数中构建的对象。
- class MyThread : public QThread
- {
- public:
- MyThread()
- {
- otherObj = new QObject;
- }
- private:
- QObject obj;
- QObject *otherObj;
- QScopedPointer<QObject> yetAnotherObj;
- };
如上述代码,我们在创建了MyThread 对象后,obj, otherObj, yetAnotherObj 的线程依附性是怎么样的?要回答这个问题,我们必须要看一下创建他们的线程:是这个运行MyThread 构造函数的线程创建了他们。因此,这三个对象并没有驻足在MyThread 线程,而是驻足在创建MyThread 实例的线程中。
要注意的是在QCoreApplication 对象之前创建的QObjects没有依附于某一个线程。因此,没有人会为它们做事件派发处理。(换句话说,QCoreApplication 构建了代表主线程的QThread 对象)
我们可以使用线程安全的QCoreApplication::postEvent() 方法来为某个对象分发事件。它将把事件加入到对象所驻足的线程事件队列中。因此,除非事件对象依附的线程有一个正在运行的事件循环,否则事件不会被派发。
理解QObject和它所有的子类不是线程安全的(尽管是可重入的)非常重要;因此,除非你序列化对象内部数据 所有可访问的接口、数据,否则你不能让多个线程同一时刻访问相同的QObject(比如,用一个锁来保护)。请注意,尽管你可以从另一个线程访问对象,但 是该对象此时可能正在处理它所驻足的线程事件循环派发给它的事件! 基于这种原因,你不能从另一个线程去删除一个QObject,一定要使用QObject::deleteLater(),它会Post一个事件,目标删除 对象最终会在它所生存的线程中被删除。(译者注:QObject::deleteLater作用是,当控制流回到该对象所依附的线程事件循环时,该对象才会被“本”线程中删除)。
此外,QWidget 和它所有的子类,以及所有与GUI相关的类(即便不是基于QObject的,像QPixmap)并不是可重入的。它们必须专属于GUI线程。
我们可以通过调用QObject::moveToThread()来改变一个QObject的依附性;它将改变这个对象以及它的孩子们的依附性。因为QObject不是线程安全的,我们必须在对象所驻足的线程中使用此函数;也就是说,你只能将对象从它所驻足的线程中推送到其他线程中,而不能从其他线程中拉回来。此外,Qt要求一个QObject的孩子必须与它们的双亲驻足在同一个线程中。这意味着:
- 你不能使用QObject::moveToThread()作用于有双亲的对象;
- 你千万不要在一个线程中创建对象的同时把QThread对象自己作为它的双亲。 (译者注:两者不在同一个线程中):
- class Thread : public QThread {
- void run() {
- QObject obj = new QObject(this); // WRONG!!!
- }
- };
这是因为,QThread 对象驻足在另一个线程中,即QThread 对象它自己被创建的那个线程中。
Qt同样要求所有的对象应该在代表该线程的QThread对象销毁之前得以删除;实现这一点并不难:只要我们所有的对象是在QThread::run() 方法中创建即可。(译者注:run函数的局部变量,函数返回时得以销毁)。
接着上面讨论的,我们如何应用驻足在其他线程里的QObject方法呢?Qt提供了一种非常友好而且干净的解决方案:向事件队列post一个事件, 事件的处理将以调用我们所感兴趣的方法为主(当然这需要线程有一个正在运行的事件循环)。而触发机制的实现是由moc提供的内省方法实现的(译者注:有关 内省的讨论请参见我的另一篇文章Qt的内省机制剖析):因此,只有信号、槽以及被标记成Q_INVOKABLE的方法才能够被其它线程所触发调用。
静态方法QMetaObject::invokeMethod() 为我们做了如下工作:
- QMetaObject::invokeMethod(object, "methodName",
- Qt::QueuedConnection,
- Q_ARG(type1, arg1),
- Q_ARG(type2, arg2));
请注意,因为上面所示的参数需要被在构建事件时进行硬拷贝,参数的自定义型别所对应的类需要提供一个共有的构造函数、析构函数以及拷贝构造函数。而且必须使用注册Qt型别系统所提供的qRegisterMetaType() 方法来注册这一自定义型别。
跨线程的信号槽的工作方式相类似。当我们把信号连接到一个槽的时候,QObject::connect的第五个可选输入参数用来特化这一连接类型:
- direct connection 是指:发起信号的线程会直接触发其所连接的槽;
- queued connection 是指:一个事件被派发到接收者所在的线程中,在这里,事件循环会之后的某一时间将该事件拾起并引起槽的调用;
- blocking queued connection 与queued connection的区别在于,发送者的线程会被阻塞,直至接收者所在线程的事件循环处理发送者发送(入栈)的事件,当连接信号的槽被触发后,阻塞被解除;
- automatic connection (缺省默认参数) 是指: 如果接收者所依附的线程和当前线程是同一个线程,direct connection会被使用。否则使用queued connection。
请注意,在上述四种连接方式当中,发送对象驻足于哪一个线程并不重要!对于automatic connection,Qt会检查触发信号的线程,并且与接收者所驻足的线程相比较从而决定到底使用哪一种连接类型。特别要指出的是:当前的Qt文档的声明(4.7.1) 是错误的:
如果发射者和接受者在同一线程,其行为与Direct Connection相同;,如果发射者和接受者不在同一线程,其行为Queued Connection相同
因为,发送者对象的线程依附性在这里无关紧要。举例子说明
- 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() 将在一个新的线程里被发射(由线程对象所代表);因为它并不是Object 对象驻足的线程,所以尽管Thread对象thread与Object对象obj在同一个线程,但仍然是queued connection被使用。
(译者注:这里作者分析的很透彻,希望读者仔细揣摩Qt文档的这个错误。 也就是说 发送者对象本身在哪一个线程对与信号槽连接类型不起任何作用,起到决定作用的是接收者对象所驻足的线程以及发射信号(该信号与接受者连接)的线程是不是在 同一个线程,本例中aSignal()在新的线程中被发射,所以采用queued connection)。
另外一个常见的错误如下:
- 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()信号是,哪种连接将被使用呢?你也许已经猜到了:direct connection。这 是因为Thread对象实在发射该信号的线程中生存。在aSlot()槽里,我们可能接着去访问线程里的一些成员变量,然而这些成员变量可能同时正在被 run()方法访问:这可是导致完美灾难的秘诀。可能你经常在论坛、博客里面找到的解决方案是在线程的构造函数里加一个 moveToThread(this)方法。
class
Thread :
public
QThread
{
Q_OBJECT
public
:
Thread() {
moveToThread(
this
);
// 错误
}
/* ... */
};
(译注:moveToThread(this)
)
这样做确实可以工作(因为现在线程对象的依附性已经发生了改变),但这是一个非常不好的设计。这里的错误在于我们正在误解线程对象的目的(QThread子类):QThread对象们不是线程;他们是围绕在新产生的线程周围用于控制管理新线程的对象,因此,它们应该用在另一个线程(往往在它们所驻足的那一个线程)
一个比较好而且能够得到相同结果的做法是将“工作”部分从“控制”部分剥离出来,也就是说,写一个QObject子类并使用QObject::moveToThread()方法来改变它的线程依附性:
- class Worker : public QObject
- {
- Q_OBJECT
- public slots:
- void doWork() {
- /* ... */
- }
- };
- /* ... */
- QThread thread;
- Worker worker;
- connect(obj, SIGNAL(workReady()), &worker, SLOT(doWork()));
- worker.moveToThread(&thread);
- thread.start();
我应该什么时候使用线程
当你需要(通过信号和槽,或者是事件、回调函数)使用一个没有提供非阻塞式API的库或者代码时,为了阻止冻结事件循环的唯一可行的解决方案是开启一个进程或者线程。由于创建一个新的进程的开销显然要比开启一个线程的开销大,后者往往是最常见的一种选择。
这种API的一个很好的例子是地址解析 方法(只是想说我们并不准备谈论蹩脚的第三方API, 地址解析方法它是每个C库都要包含的),它负责将主机名转化为地址。这个过程涉及到启动一个查询(通常是远程的)系统:域名系统或者叫DNS。尽管通常情 况下响应会在瞬间发生,但远程服务器可能会失败:一些数据包可能会丢失,网络连接可能断开等等。简而言之,我们也许要等待几十秒才能得到查询的响应。
UNIX系统可见的标准API只有阻塞式的(不仅过时的gethostbyname(3)是阻塞式的,而且更新的getservbyname(3) 以及getaddrinfo(3)也是阻塞式的)。QHostInfo [doc.qt.nokia.com], 它是一个负责处理域名查找的Qt类,该类使用了QThreadPool 从而使得查询可以在后台进行)(参见here[qt.gitorious.com]);如果屏蔽了多线程支持,它将切换回到阻塞式API).
另一个简单的例子是图像装载和放大。QImageReader [doc.qt.nokia.com] 和QImage [doc.qt.nokia.com]仅仅提供了阻塞式方法来从一个设备读取图像,或者放大图像到一个不同的分辨率。如果你正在处理一个非常大的图像,这些处理会持续数(十)秒。
多线程允许你的程序利用多核系统的优势。因为每个线程都是被操作系统独立调度的,因此如果你的应用运行在这样多核机器上,调度器很可能同时在不同的处理器上运行每个线程。
例如,考虑到一个通过图像集生成缩略图的应用。一个_n_ threads的线程农场(也就是说,一个有着固定 数量线程的线程池),在系统中可见的CPU运行一个线程(可参见QThread::idealThreadCount()),可以将缩小图像至缩略图的工 作交付给所有的进程,从而有效地提高了并行加速比,它与处理器的数量成线性关系。(简单的讲,我们认为CPU正成为一个瓶颈)。
这是一个很高级的话题,你可以忽略该小节。一个比较好的例子来自于Webkit里使用的QNetworkAccessManager 。Webkit是一个时髦的浏览器引擎,也就是说,它是一组用于处理网页的布局和显示的类集合。使用Webkit的Qt widget是QWebView。
QNetworkAccessManager 是一个用于处理HTTP任何请求和响应的Qt类,我们可以把它当作一个web浏览器的网络引擎;所有的网络访问被同一个QNetworkAccessManager 以及它的QNetworkReplys 驻足的线程所处理。
尽管在网络处理时不使用线程是一个很好的主意,它也有一个很大的缺点:如果你没有从socket中尽快地读取数据,内核的缓存将会被填满,数据包可能开始丢失而且传输速率也将迅速下降。
Sokcet活动(即,从一个socket读取一些数据的可见性)由Qt的事件循环管理。阻塞事件循环因此会导致传输性能的损失,因为没有人会被通知将有数据可以读取(从而没人会去读数据)。
但究竟什么会阻塞事件循环呢?令人沮丧地回答: WebKit它自己!只要有数据被接收到,WebKit便用其来布局网页。不幸地是,布局处理过程相当复杂,而且开销巨大。因此,它阻塞事件循环的一小段 时间足以影响到正在进行地传输(宽带连接这里起到了作用,在短短几秒内就可填满内核缓存)。
总结一下上述所发生的事情:
- WebKit提出了一个请求;
- 一些响应数据开始到达;
- WebKit开始使用接收到的数据布局网页,从而阻塞了事件循环;
- 数据被OS接受,但没有一个正在运行的事件循环为之派发,所以并没有被QNetworkAccessManager sockets所读取;
- 内核缓存将被填满,传输将变慢。
网页的总体装载时间因其自发引起的传输速率降低而变得越来越坏。
诺基亚的工程师正在试验一个支持多线程的QNetworkAccessManager来解决这个问题。请注意因为 QNetworkAccessManagers 和QNetworkReplys 是QObjects,他们不是线程安全的,因此你不能简单地将他们移到另一个线程中并且继续在你的线程中使用他们,原因在于,由于事件将被随后线程的事件 循环所派发,他们可能同时被两个线程访问:你自己的线程以及已经它们驻足的线程。
是么时候不需要使用线程
If you think you need threads then your processes are too fat.—Rob Pike
这也许是线程滥用最坏的一种形式。如果我们不得不重复调用一个方法(比如每秒),许多人会这样做:
- // 非常之错误
- while (condition) {
- doWork();
- sleep(1); // this is sleep(3) from the C library
- }
然后他们发现这会阻塞事件循环,因此决定引入线程:
- // 错误
- class Thread : public QThread {
- protected:
- void run() {
- while (condition) {
- // notice that "condition" may also need volatiness and mutex protection
- // if we modify it from other threads (!)
- doWork();
- sleep(1); // this is QThread::sleep()
- }
- }
- };
一个更好也更简单的获得相同效果的方法是使用timers,即一个QTimer[doc.qt.nokia.com]对象,并设置一秒的超时时间,并让doWork方法成为它的槽:
- class Worker : public QObject
- {
- Q_OBJECT
- public:
- Worker() {
- connect(&timer, SIGNAL(timeout()), this, SLOT(doWork()));
- timer.start(1000);
- }
- private slots:
- void doWork() {
- /* ... */
- }
- private:
- QTimer timer;
- };
所有我们需要做的就是运行一个事件循环,然后doWork()方法将会被每隔秒钟调用一次。
一个处理网络操作非常之常见的设计模式如下:
- socket->connect(host);
- socket->waitForConnected();
- data = getData();
- socket->write(data);
- socket->waitForBytesWritten();
- socket->waitForReadyRead();
- socket->read(response);
- reply = process(response);
- socket->write(reply);
- socket->waitForBytesWritten();
- /* ... and so on ... */
不用多说,各种各样的waitFor*()函数阻塞了调用者使其无法返回到事件循环,UI被冻结等等。请注意上面的这段代码并没有考虑到错误处理,否则它会更加地笨重。这个设计中非常错误的地方是我们正在忘却网络编程是异步的设计,如果我们构建一个同步的处理方法,则是自己给自己找麻烦。为了解决这个问题,许多人简单得将这些代码移到另一个线程中。
另一个更加抽象的例子:
- result = process_one_thing();
- if (result->something())
- process_this();
- else
- process_that();
- wait_for_user_input();
- input = read_user_input();
- process_user_input(input);
- /* ... */
它多少反映了网络编程相同的陷阱。
让我们回过头来从更高的角度来想一下我们这里正在构建的代码:我们想创造一个状态机,用以反映某类的输入并相对应的作某些动作。比如,上面的这段网络代码,我们可能想做如下这些事情:
- 空闲→ 正在连接 (当调用connectToHost());
- 正在连接→ 已经连接(当connected() 信号被发射);
- 已经连接→ 发送录入数据 (当我们发送录入的数据给服务器);
- 发送录入数据 → 录入 (服务器响应一个ACK)
- 发送录入数据→ 录入错误(服务器响应一个NACK)
以此类推。
现在,有很多种方式来构建状态机(Qt甚至提供了QStateMachine[doc.qt.nokia.com]类),最简单的方式是用一个枚举值(及,一个整数)来记忆当前的状态。我们可以这样重写以下上面的代码:
- class Object : public QObject
- {
- Q_OBJECT
- enum State {
- State1, State2, State3 /* and so on */
- };
- State state;
- public:
- Object() : state(State1)
- {
- connect(source, SIGNAL(ready()), this, SLOT(doWork()));
- }
- private slots:
- void doWork() {
- switch (state) {
- case State1:
- /* ... */
- state = State2;
- break;
- case State2:
- /* ... */
- state = State3;
- break;
- /* etc. */
- }
- }
- };
那么“souce”对象和它的信号“ready()” 究竟是什么? 我们想让它们是什么就是什么:比如说,在这个例子中,我们可能想把我们的槽连接到socket的 QAbstractSocket::connected() 以及QIODevice::readyRead() 信号中,当然,我们也可以简单地在我们的用例中加更多的槽(比如一个槽用于处理错误情况,它将会被QAbstractSocket::error() 信号所通知)。这是一个真正的异步的,信号驱动的设计!
假如我们有一个开销很大的计算,它不能够轻易的移到另一个线程中(或者说它根本不能被移动,举个例子,它必须运行在GUI线程中)。如果我们能将计算拆分成小的块,我们就能返回到事件循环,让它来派发事件,并让它激活处理下一个块相应的函数。如果我们还记得queued connections是怎么实现的,那么会觉得这是很容易能够做到的:一个事件派发到接收者所驻足的线程的事件循环;当事件被传递,相应的槽随之被激活。
我们可以使用特化QMetaObject::invokeMethod() 的激活类型为Qt::QueuedConnection 来得到相同的结果;这需要函数是可激活的。因此它需要一个槽或者用Q_INVOKABLE宏来标识。如果我们同时想给函数中传入参数,他们需要使用Qt元 对象类型系统里的qRegisterMetaType()进行注册。请看下面这段代码:
- class Worker : public QObject
- {
- Q_OBJECT
- public slots:
- void startProcessing()
- {
- processItem(0);
- }
- void processItem(int index)
- {
- /* process items[index] ... */
- if (index < numberOfItems)
- QMetaObject::invokeMethod(this,
- "processItem",
- Qt::QueuedConnection,
- Q_ARG(int, index + 1));
- }
- };
/*****************************************************************
前言
因为在图像处理的过程中,通过信号与槽避过了需要创建线程的麻烦,但是之后程序的运行出现卡顿的现象。所以想对信号与槽的机制进行了解,但感觉好像和Qt的事件循环有关系,Qt也只是入门而已!对线程的使用和对事件循环的理解感觉是一个很重要的话题!我们到底应该怎样使用信号与槽?怎样使用线程?
学习!分享!感谢!
介绍
Qt中创建、运行线程的“易用”性、缺乏相关编程尤其是异步网络编程知识或是养成的使用其它工具集的习惯、这些因素和Qt的信号槽架构混合在一起,便经常使得人们自己把自己射倒在了脚下。
- 可重入
一个类被称为可重入的:只要在同一时刻至多只有一个线程访问同一个实例,那么我们说多个线程可以安全地使用各自线程内自己的实例。一个函数被称为是可重入的:如果每一次函数的调用只访问其独有的数据,那么我们说多个线程可以安全的调用这个函数。也就是说,类和函数的使用者必须通过一些外部的加锁机制来实现访问对象实例或共享数据的序列化。 - 线程安全
如果多个线程可以同时使用一个类的对象,那么这个类被称为是线程安全的;如果多个线程可以同时使用一个函数体里的共享数据,那么这个函数被称为线程安全的。对于 类,如果它的 成员函数都可以被不同的线程同时调用而不相互影响,即使这些调用是针对同一个类对象,那么该类被定义为线程安全。对于类,如果其不同实例可以在不同线程中被同时使用而不相互影响,那么该类定义为可重入。
事件和事件循环
一个Qt事件代表了某件令人感兴趣并已经发生的事件,比如鼠标事件; 事件与信号的主要区别在于,**事件**是针对于我们应用中一个**具体目标对象**(而这个对象决定了我们如何处理这个事件),比如鼠标就是一个具体的目标对象,对于这个对象,有按下、弹起、移动等事件,这样我们针对这个事件提供对应的事件处理方法。而信号发射时“漫无目的”的。从代码的角度来说,所有的事件实例是`QEvent`的子类,并且所有的`QObject`的派生类可以重载虚函数`QObject::event()`,从而实现对目标对象实例事件的处理。 事件可以产生于应用程序的内部,也可以来源于外部,比如:
- QKeyEvent和QMouseEvent对象代表了与键盘、鼠标相关的交互事件,它们来自于视窗管理程序。
- 当计时器开始计时,QTimerEvent 对象被发送到QObject对象中,它们往往来自于操作系统。
- 当一个子类对象被添加或删除时,QChildEvent对象会被发送到一个QObject对象中,而它们来自于你的应用程序内部。
对于事件来讲,一个重要的事情在于它们并没有在事件产生时被立即派发,而是列入到一个事件队列中,等待以后的某个时刻发送。分配器(dispatcher)会遍历事件队列,并且将入栈的事件发送到目标对象中,因此它们被称为事件循环。从概念上讲,下段代码描述了一个事件循环的轮廓:
<span style="color:#000000"><span style="background-color:#f6f8fa"><code><span style="color:#000088 !important">while</span> (is_active)
{
<span style="color:#000088 !important">while</span> (!event_queue_is_empty)
dispatch_next_event(); // dispatch 调度
wait_<span style="color:#000088 !important">for</span>_more_events();
} </code></span></span>
我们是通过运行QCoreApplication::exec()
来进入Qt
的主体事件循环的;这会引发阻塞,直至QCoreApplication::exit()
或者 QCoreApplication::quit()
被调用,进而结束循环。
这个wait_for_more_events()
函数产生阻塞,直至某个事件的产生。 如果我们仔细想想,会发现所有在那个时间点产生事件的实体必定是来自于外部的资源(因为当前所有内部事件派发已经结束,事件队列里也没有悬而未决的事件等待处理),因此事件循环被这样唤醒(也就是某个时刻我们应该保证Qt
的主线程保持没有内部事件在运行,等待着响应外部事件):
- 视窗管理活动(键盘按键、鼠标点击,与视窗的交互等等);
- socket活动 (有可见的用来读取的数据或者一个可写的非阻塞Socket, 一个新的Socket连接的产生);
- timers (即计时器开始计时)
- 其它线程Post的事件
为什么需要事件循环?
需要使用事件循环的类
Widgets
绘图与交互: 当派发QPaintEvent
事件时,QWidget::paintEvent()
将会被调用。QPaintEvent
可以产生于内部的QWidget::update()
,也可以产生于外部的视窗管理(比如,一个显示被隐藏的窗口)。同样的,各种各样的交互(键盘、鼠标等)所对应的事件均需要事件循环来派发Timers
:计时器超时后,让Qt
通过返回事件循环让那些调用为你工作。Networking
阻塞事件循环
永远不要阻塞事件循环,假定你有一个按钮button,它被按下时会emit一个信号;还有我们定义了一个Worker对象连接了这个信号,而且这个对象的槽做了很多耗时的事情。当你点击完这个按钮后,从上至下的函数调用栈如下所示:
<span style="color:#000000"><span style="background-color:#f6f8fa"><code class="language-c++">main(int, char **)
QApplication::exec()
[<span style="color:#000088 !important">...</span>]
QWidget::event(QEvent *)
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
[<span style="color:#000088 !important">...</span>]
Worker::doWork()</code></span></span>
在main()
中,我们通过调用QApplication::exec()
。开启了事件循环。视窗管理者发送了鼠标点击事件,该事件被Qt
内核捕获,并转换成QMouseEvent
,随后通过QApplication::notify()
(notify
并没有在上述代码里显示)发送到我们的widget
的event()
方法中。因为Button
并没有重载event()
,它的基类QWidget
方法得以调用。 QWidget::event()
检测出传入的事件是一个鼠标点击,并调用其专有的事件处理器,即Button::mousePressEvent()
。我们重载了 mousePressEvent
方法,并发射了Button::clicked()
信号,该信号激活了我们worker
对象中十分耗时的Worker::doWork()
槽。当worker
对象在繁忙的工作时,事件循环将什么也不做,它分发了鼠标点击事件,并且因等待event handler
返回而被阻塞。我们阻塞了事件循环,也就是说,在我们的doWork()
槽干完活之前再不会有事件派发了,也不会有挂起的事件被处理。当事件派发被就此卡住时,widgets
也将不会再刷新自己(QPaintEvent
对象将在事件队列中静候),也不能有进一步地与widgets
交互的事件发生,计时器也不会再开始计时,网络通讯也将变得迟钝、停滞。更严重的是,许多视窗管理程序会检测到你的应用不再处理事件,从而告诉用户你的程序不再有响应(not responding
). 这就是为什么快速的响应事件并尽可能快的返回事件循环如此重要的原因。
强制事件循环
对于需要长时间运行的任务,我们应该怎么做才会不阻塞事件循环? 一个可行的答案是将这个任务移动另一个线程中;一个可能的方案是,在我们的受阻塞的任务中,通过调用QCoreApplication::processEvents()
人工地强迫事件循环运行;另一个可选的强制地重入事件的方案是使用QEventLoop
类,通过调用QEventLoop::exec()
,我们重入了事件循环,而且我们可以把信号连接到QEventLoop::quit()
槽上使得事件循环退出。
如何强制事件循环
- 将这个任务移动到另一个线程中
- 在我们受阻塞的任务中,通过调用
QCoreApplication::processEvents()
人工强迫事件循环运行 - 通过调用
QEventLoop::exec()
,重入事件循环,而且可以把信号连接到QEventLoop::quit()
槽上使得事件循环退出。
<span style="color:#000000"><span style="background-color:#f6f8fa"><code>QNetworkAccessManager qnam;
QNetworkReply *reply = qnam.get(QNetworkRequest(QUrl(<span style="color:#000088 !important">...</span>)));
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
loop.exec();
// QNetworkReply没有提供一个阻塞式的API,而且它要求一个事件循环。我们进入到一个局部的,并且当回应完成时,局部事件循环退出</code></span></span>
博主记:简单理解了一下在事件循环中不使用线程来做耗时操作,不过目前博主的开发多数都是使用线程,所以更深入的不解释。
Qt线程类
QThread
QThread
是Qt
中一个对线程支持的核心底层类。 每个线程对象代表了一个运行的线程。由于Qt
的跨平台特性,QThread
成功隐藏了所有在不同操作系统里使用线程的平台相关性代码。为了运用QThread
从而让代码在一个线程里运行,我们可以创建一个QThread
的子类,并重载QThread::run()
方法:
<span style="color:#000000"><span style="background-color:#f6f8fa"><code>class Thread : <span style="color:#000088 !important">public</span> QThread {
<span style="color:#000088 !important">protected</span>:
<span style="color:#000088 !important">void</span> <span style="color:#009900 !important">run</span>() {
<span style="color:#880000 !important"><em>/* your thread implementation goes here */</em></span>
}
}; </code></span></span>
Qt 4.4
版本之后,QThread
不再支持抽象类;现在虚函数QThread::run()
实际上是简单调用了QThread::exec()
,而它启动了线程的事件循环。
- 举例:
<span style="color:#000000"><span style="background-color:#f6f8fa"><code><span style="color:#000088 !important">class</span> <span style="color:#4f4f4f !important">WorkerThread</span> : public <span style="color:#4f4f4f !important">QThread</span>
{
<span style="color:#4f4f4f !important">Q_OBJECT</span>
void run() <span style="color:#4f4f4f !important">Q_DECL_OVERRIDE</span> {
<span style="color:#4f4f4f !important">QString</span> result;
emit resultReady(<span style="color:#4f4f4f !important">result</span>);
}
signals:
void resultReady(<span style="color:#4f4f4f !important">const</span> <span style="color:#4f4f4f !important">QString</span> &<span style="color:#4f4f4f !important">s</span>);
};
void <span style="color:#4f4f4f !important">MyObject</span>::startWorkInAThread()
{
<span style="color:#4f4f4f !important">WorkerThread</span> *workerThread = new <span style="color:#4f4f4f !important">WorkerThread</span>(<span style="color:#4f4f4f !important">this</span>);
connect(<span style="color:#4f4f4f !important">workerThread</span>, &<span style="color:#4f4f4f !important">WorkerThread</span>::<span style="color:#4f4f4f !important">resultReady</span>, <span style="color:#4f4f4f !important">this</span>, &<span style="color:#4f4f4f !important">MyObject</span>::<span style="color:#4f4f4f !important">handleResults</span>);
connect(<span style="color:#4f4f4f !important">workerThread</span>, &<span style="color:#4f4f4f !important">WorkerThread</span>::<span style="color:#4f4f4f !important">finished</span>, <span style="color:#4f4f4f !important">workerThread</span>, &<span style="color:#4f4f4f !important">QObject</span>::<span style="color:#4f4f4f !important">deleteLater</span>);
workerThread->start(); // 自动调用run()函数
}</code></span></span>
在这个例子中,线程中将不会有任何的事件循环运行除非调用exec()
。注意,在一个线程实例位于实例化它的旧线程中,而非调用run()
的新线程中,这意味着所有线程的排队槽将在旧线程中执行。在子类化QThread
时,构造函数在旧线程中执行,而run()
在新线程中执行。
线程与QObjects
线程的事件循环
QThread
对象在它们所代表的线程中开启了新的事件循环。因此,我们说main
事件循环是由调用main()
的线程通过QCoreApplication::exec()
创建的。main线程
也被称为GUI
线程。因为它是界面相关操作的唯一允许的进程。一个QThread
的局部事件循环可以通过调用QThread::exec()
来开启(它包含在run()
方法内部)。
<span style="color:#000000"><span style="background-color:#f6f8fa"><code>class Thread : public QThread {
protected:
void run() {
/* <span style="color:#000088 !important">...</span> initialize <span style="color:#000088 !important">...</span> */
exec();
}
}; </code></span></span>
正如我们之前所提到的,自从Qt 4.4
的QThread::run()
方法不再是一个纯虚函数,它调用了QThread::exec()
。就像QCoreApplication
,QThread
也有QThread::quit()
和QThread::exit()
来停止事件循环。
一个线程的事件循环为驻足在该线程中的所有QObjects
派发了所有事件,其中包括在这个线程中创建的所有对象,或者移植到这个线程中的对象。QObject
的依附性(thread affinity
)是指某一个线程,该对象驻足在该线程内。
<span style="color:#000000"><span style="background-color:#f6f8fa"><code>class MyThread : <span style="color:#000088 !important">public</span> QThread
{
<span style="color:#000088 !important">public</span>:
<span style="color:#009900 !important">MyThread</span>()
{
otherObj = <span style="color:#000088 !important">new</span> QObject;
}
<span style="color:#000088 !important">private</span>:
QObject obj;
QObject *otherObj;
QScopedPointer<QObject> yetAnotherObj;
}; </code></span></span>
在我们创建了MyThread
对象后,obj
,otherobj
,yetAnotherObj
是在运行MyThread
构造函数的线程中创建的。因此,这三个对象没有驻足在MyThread
线程中,而是驻足在创建MyThread
实例的线程中。
注意:在QCoreApplication
对象之前创建的QObjects
没有依附于某一个线程。因此,没有人会为它们做事件派发处理(换句话说,QCoreApplication
构建了代表主线程的QThread
对象)
使用线程安全的QCoreApplication::postEvent()方
法来为某个对象分发事件。它将把事件加入到对象所驻足的线程事件队列中。因此,除非事件对象依附的线程有一个正在运行的事件循环,否则事件不会被派发。
理解QObject
和它所有的子类不是线程安全的(尽管可重入);因此,除非你序列化对象内部数据所有可访问的接口、数据,否则你不能让多个线程同一时刻访问相同的QObject
(比如,加锁)。
注意:尽管你可以另一个线程访问对象,但是该对象此时可能正在处理它所驻足的线程事件循环派发给它的事件!基于这种原因,你不能从另一个线程中删除一个QObject
,一定要使用QObject::deleteLater()
,它会Post
一个事件,目标删除对象最终会在它所生存的线程中被删除。(QObject::deleteLater
的作用是当控制流回到该对象所依附的线程事件循环时,该对象才会被”本”线程删除)。
比如:
<span style="color:#000000"><span style="background-color:#f6f8fa"><code><span style="color:#006666 !important">void</span> Widget<span style="color:#006666 !important">::startNetStreamThread</span>()
{
netThread <span style="color:#4f4f4f !important">=</span> <span style="color:#006666 !important">new</span> QThread();
netStrThread <span style="color:#4f4f4f !important">=</span> <span style="color:#006666 !important">new</span> netStreamThread();
netStrThread<span style="color:#4f4f4f !important">-></span>moveToThread(netThread);
connect(netThread, <span style="color:#4f4f4f !important">&</span>QThread<span style="color:#006666 !important">::finished</span>, netThread, <span style="color:#4f4f4f !important">&</span>QObject<span style="color:#006666 !important">::deleteLater</span>);
connect(netThread, <span style="color:#4f4f4f !important">&</span>QThread<span style="color:#006666 !important">::finished</span>, netStrThread, <span style="color:#4f4f4f !important">&</span>QObject<span style="color:#006666 !important">::deleteLater</span>);
}</code></span></span>
其中netStrThread
是通过继承QObject
创建的线程,使用moveToThread
的方法放置到netThread
中运行。而netThread
对象和netStrThread
对象都是在主线程,也就是widget
线程中创建的,所以最好不要在主线程中直接删除netThread
线程,而要等到netThread
线程执行完成,回到netThread
线程的事件循环的时候,在进行删除,从而避免出错。同时netStrThread
线程是在netThread
线程中运行的,所以可以等到回到netStrThread
线程中时来删除,从而避免出现错误。
我应该在什么时候使用线程
当你不得不使用一个阻塞式API时
当你需要(通过信号和槽,或者是事件、回调函数)使用一个没有提供非阻塞式API的库或者代码时,为了阻止冻结事件循环的唯一可行的解决方案是开启一个进程或者线程。由于创建一个新的进程的开销显然要比开启一个线程的开销大,后者往往是最常见的一种选择。
博主记:也就是说,如果在主线程中发生了阻塞,这时候事件循环就会停止响应。
当你向扩展至多核
多线程允许你的程序利用多核系统的优势。因为每个线程都是被操作系统独立调度的,因此如何你的应用运行在这样多核机器上,调度器很可能同时在不同的处理器上运行每个线程。
/******************************************************
Qt 多线程之可重入与线程安全是本节要介绍的内容。在Qt文档中,术语“可重入”与“线程安全”被用来说明一个函数如何用于多线程程序。假如一个类的任何函数在此类的多个不同的实例上,可以被多个线程同时调用,那么这个类被称为是“可重入”的。假如不同的线程作用在同一个实例上仍可以正常工作,那么称之为“线程安全”的。
大多数c++类天生就是可重入的,因为它们典型地仅仅引用成员数据。任何线程可以在类的一个实例上调用这样的成员函数,只要没有别的线程在同一个实例上调用这个成员函数。举例来讲,下面的Counter 类是可重入的:
class Counter
{
public:
Counter() {n=0;}
void increment() {++n;}
void decrement() {--n;}
int value() const {return n;}
private:
int n;
};
这个类不是线程安全的,因为假如多个线程都试图修改数据成员 n,结果未定义。这是因为c++中的++和--操作符不是原子操作。实际上,它们会被扩展为三个机器指令:
1,把变量值装入寄存器
2,增加或减少寄存器中的值
3,把寄存器中的值写回内存
假如线程A与B同时装载变量的旧值,在寄存器中增值,回写。他们写操作重叠了,导致变量值仅增加了一次。很明显,访问应该串行化:A执行123步骤时不应被打断。使这个类成为线程安全的最简单方法是使用QMutex来保护数据成员:
class Counter
{
public:
Counter() { n = 0; }
void increment() { QMutexLocker locker(&mutex); ++n; }
void decrement() { QMutexLocker locker(&mutex); --n; }
int value() const { QMutexLocker locker(&mutex); return n; }
private:
mutable QMutex mutex;
int n;
};
QMutexLocker类在构造函数中自动对mutex进行加锁,在析构函数中进行解锁。随便一提的是,mutex使用了mutable关键字来修饰,因为我们在value()函数中对mutex进行加锁与解锁操作,而value()是一个const函数。
大多数Qt类是可重入,非线程安全的。有一些类与函数是线程安全的,它们主要是线程相关的类,如QMutex,QCoreApplication::postEvent()。
线程与QObjects
QThread 继承自QObject,它发射信号以指示线程执行开始与结束,而且也提供了许多slots。更有趣的是,QObjects可以用于多线程,这是因为每个线程被允许有它自己的事件循环。
QObject 可重入性
QObject是可重入的。它的大多数非GUI子类,像QTimer,QTcpSocket,QUdpSocket,QHttp,QFtp,QProcess也是可重入的,在多个线程中同时使用这些类是可能的。需要注意的是,这些类被设计成在一个单线程中创建与使用,因此,在一个线程中创建一个对象,而在另外的线程中调用它的函数,这样的行为不能保证工作良好。有三种约束需要注意:
1,QObject的孩子总是应该在它父亲被创建的那个线程中创建。这意味着,你绝不应该传递QThread对象作为另一个对象的父亲(因为QThread对象本身会在另一个线程中被创建)
2,事件驱动对象仅仅在单线程中使用。明确地说,这个规则适用于"定时器机制“与”网格模块“,举例来讲,你不应该在一个线程中开始一个定时器或是连接一个套接字,当这个线程不是这些对象所在的线程。
3,你必须保证在线程中创建的所有对象在你删除QThread前被删除。这很容易做到:你可以run()函数运行的栈上创建对象。
尽管QObject是可重入的,但GUI类,特别是QWidget与它的所有子类都是不可重入的。它们仅用于主线程。正如前面提到过的,QCoreApplication::exec()也必须从那个线程中被调用。实践上,不会在别的线程中使用GUI类,它们工作在主线程上,把一些耗时的操作放入独立的工作线程中,当工作线程运行完成,把结果在主线程所拥有的屏幕上显示。
/*******************************************************
每个线程可以有它的事件循环,初始线程开始它的事件循环需使用QCoreApplication::exec(),别的线程开始它的事件循环需要用QThread::exec().像QCoreApplication一样,QThreadr提供了exit(int)函数,一个quit() slot。
线程中的事件循环,使得线程可以使用那些需要事件循环的非GUI 类(如,QTimer,QTcpSocket,QProcess)。也可以把任何线程的signals连接到特定线程的slots,也就是说信号-槽机制是可以跨线程使用的。对于在QApplication之前创建的对象,QObject::thread()返回0,这意味着主线程仅为这些对象处理投递事件,不会为没有所属线程的对象处理另外的事件。可以用QObject::moveToThread()来改变它和它孩子们的线程亲缘关系,假如对象有父亲,它不能移动这种关系。在另一个线程(而不是创建它的那个线程)中delete QObject对象是不安全的。除非你可以保证在同一时刻对象不在处理事件。可以用QObject::deleteLater(),它会投递一个DeferredDelete事件,这会被对象线程的事件循环最终选取到。
假如没有事件循环运行,事件不会分发给对象。举例来说,假如你在一个线程中创建了一个QTimer对象,但从没有调用过exec(),那么QTimer就不会发射它的timeout()信号.对deleteLater()也不会工作。(这同样适用于主线程)。你可以手工使用线程安全的函数QCoreApplication::postEvent(),在任何时候,给任何线程中的任何对象投递一个事件,事件会在那个创建了对象的线程中通过事件循环派发。事件过滤器在所有线程中也被支持,不过它限定被监视对象与监视对象生存在同一线程中。类似地,QCoreApplication::sendEvent(不是postEvent()),仅用于在调用此函数的线程中向目标对象投递事件。
从别的线程中访问QObject子类
QObject和所有它的子类是非线程安全的。这包括整个的事件投递系统。需要牢记的是,当你正从别的线程中访问对象时,事件循环可以向你的QObject子类投递事件。假如你调用一个不生存在当前线程中的QObject子类的函数时,你必须用mutex来保护QObject子类的内部数据,否则会遭遇灾难或非预期结果。像其它的对象一样,QThread对象生存在创建它的那个线程中---不是当QThread::run()被调用时创建的那个线程。一般来讲,在你的QThread子类中提供slots是不安全的,除非你用mutex保护了你的成员变量。
另一方面,你可以安全的从QThread::run()的实现中发射信号,因为信号发射是线程安全的。
跨线程的信号-槽
Qt支持三种类型的信号-槽连接:
1,直接连接,当signal发射时,slot立即调用。此slot在发射signal的那个线程中被执行(不一定是接收对象生存的那个线程)
2,队列连接,当控制权回到对象属于的那个线程的事件循环时,slot被调用。此slot在接收对象生存的那个线程中被执行
3,自动连接(缺省),假如信号发射与接收者在同一个线程中,其行为如直接连接,否则,其行为如队列连接。
连接类型可能通过以向connect()传递参数来指定。注意的是,当发送者与接收者生存在不同的线程中,而事件循环正运行于接收者的线程中,使用直接连接是不安全的。同样的道理,调用生存在不同的线程中的对象的函数也是不是安全的。QObject::connect()本身是线程安全的。
多线程与隐含共享
Qt为它的许多值类型使用了所谓的隐含共享(implicit sharing)来优化性能。原理比较简单,共享类包含一个指向共享数据块的指针,这个数据块中包含了真正原数据与一个引用计数。把深拷贝转化为一个浅拷贝,从而提高了性能。这种机制在幕后发生作用,程序员不需要关心它。如果深入点看,假如对象需要对数据进行修改,而引用计数大于1,那么它应该先detach()。以使得它修改不会对别的共享者产生影响,既然修改后的数据与原来的那份数据不同了,因此不可能再共享了,于是它先执行深拷贝,把数据取回来,再在这份数据上进行修改。例如:
void QPen::setStyle(Qt::PenStyle style)
{
detach(); // detach from common data
d->stylestyle = style; // set the style member
}
void QPen::detach()
{
if (d->ref != 1) {
... // perform a deep copy
}
}
一般认为,隐含共享与多线程不太和谐,因为有引用计数的存在。对引用计数进行保护的方法之一是使用mutex,但它很慢,Qt早期版本没有提供一个满意的解决方案。从4.0开始,隐含共享类可以安全地跨线程拷贝,如同别的值类型一样。它们是完全可重入的。隐含共享真的是"implicit"。它使用汇编语言实现了原子性引用计数操作,这比用mutex快多了。
假如你在多个线程中同进访问相同对象,你也需要用mutex来串行化访问顺序,就如同其他可重入对象那样。总的来讲,隐含共享真的给”隐含“掉了,在多线程程序中,你可以把它们看成是一般的,非共享的,可重入的类型,这种做法是安全的。