【转】Qt事件循环与线程 二

转自:http://blog.csdn.net/changsheng230/article/details/6153449html

续上文:http://blog.csdn.net/changsheng230/archive/2010/12/27/6101232.aspxgit

 

因为最近工做比较忙,出了趟差,仍是把这篇长文、好文翻译出来了,以飨读者。同时也是本身很好的消化、学习过程web

Qt 线程类

Qt对线程的支持已经有不少年了(发布于2000年九月22日的Qt2.2引入了QThread类),Qt 4.0版本的release则对其全部所支持平台默认地是对多线程支持的。(固然你也能够关掉对线程的支持,参见这里)。如今Qt提供了很多类用于处理线程,让你咱们首先预览一下:编程

QThread

QThread 是Qt中一个对线程支持的核心的底层类。 每一个线程对象表明了一个运行的线程。因为Qt的跨平台特性,QThread成功隐藏了全部在不一样操做系统里使用线程的平台相关性代码。设计模式

为了运用QThread从而让代码在一个线程里运行,咱们能够建立一个QThread的子类,并重载QThread::run() 方法:浏览器

 

 

  1. class Thread : public QThread {  
  2. protected:  
  3. void run() {  
  4. /* your thread implementation goes here */  
  5. }  
  6. };  

 

接着,咱们可使用:缓存

 

  1. class Thread : public QThread {  
  2. protected:  
  3. void run() {  
  4. /* your thread implementation goes here */  
  5. }  
  6. };  

 

 


来真正的启动一个新的线程。 请注意,Qt 4.4版本以后,QThread再也不支持抽象类;如今虚函数QThread::run()其实是简单调用了QThread::exec(),而它启动了线程的事件循环。(更多信息见后文)安全

QRunnable 和 QThreadPool

QRunnable [doc.qt.nokia.com] 是一种轻量级的、以“run and forget”方式来在另外一个线程开启任务的抽象类,为了实现这一功能,咱们所须要作的所有事情是派生QRunnable 类,并实现纯虚函数方法run()服务器

 

  1. class Task : public QRunnable {  
  2. public:  
  3. void run() {  
  4. /* your runnable implementation goes here */  
  5. }  
  6. };  

 

事实上,咱们是使用QThreadPool 类来运行一个QRunnable 对象,它维护了一个线程池。经过调用QThreadPool::start(runnable) ,咱们把一个QRunnable 放入了QThreadPool的运行队列中;只要线程是可见得,QRunnable 将会被拾起而且在那个线程里运行。尽管全部的Qt应用程序都有一个全局的线程池,且它是经过调用 QThreadPool::globalInstance()可见得,但咱们老是显式地建立并管理一个私有的QThreadPool 实例。网络

请注意,QRunnable 并非一个QObject类,它并无一个内置的与其余组件显式通信的方法。你必须使用底层的线程原语(好比收集结构的枷锁保护队列等)来亲自编写代码。

QtConcurrent

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()方法的内部)

 

  1. class Thread : public QThread {  
  2. protected:  
  3. void run() {  
  4. /* ... initialize ... */  
  5. exec();  
  6. }  
  7. };  

 

正如咱们以前所提到的,自从Qt 4.4 的QThread::run() 方法再也不是一个纯虚函数,它调用了QThread::exec()。就像QCoreApplication,QThread 也有QThread::quit() 和QThread::exit()来中止事件循环。

一个线程的事件循环为驻足在该线程中的全部QObjects派发了全部事件,其中包括在这个线程中建立的全部对象,或是移植到这个线程中的对象。咱们说一个QObject的线程依附性(thread affinity)是指某一个线程,该对象驻足在该线程内。咱们在任什么时候间均可以经过调用QObject::thread()来查询线程依附性,它适用于在QThread对象构造函数中构建的对象。

 

  1. class MyThread : public QThread  
  2. {  
  3. public:  
  4. MyThread()  
  5. {  
  6. otherObj = new QObject;  
  7. }     
  8. private:  
  9. QObject obj;  
  10. QObject *otherObj;  
  11. QScopedPointer<QObject> yetAnotherObj;  
  12. };  

 

如上述代码,咱们在建立了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对象本身做为它的双亲。 (译者注:二者不在同一个线程中):

 

  1. class Thread : public QThread {  
  2. void run() {  
  3. QObject obj = new QObject(this); // WRONG!!!  
  4. }  
  5. };  

 

这是由于,QThread 对象驻足在另外一个线程中,即QThread 对象它本身被建立的那个线程中。

Qt一样要求全部的对象应该在表明该线程的QThread对象销毁以前得以删除;实现这一点并不难:只要咱们全部的对象是在QThread::run() 方法中建立便可。(译者注:run函数的局部变量,函数返回时得以销毁)。

跨线程的信号与槽

接着上面讨论的,咱们如何应用驻足在其余线程里的QObject方法呢?Qt提供了一种很是友好并且干净的解决方案:向事件队列post一个事件, 事件的处理将以调用咱们所感兴趣的方法为主(固然这须要线程有一个正在运行的事件循环)。而触发机制的实现是由moc提供的内省方法实现的(译者注:有关 内省的讨论请参见个人另外一篇文章Qt的内省机制剖析):所以,只有信号、槽以及被标记成Q_INVOKABLE的方法才可以被其它线程所触发调用。

静态方法QMetaObject::invokeMethod() 为咱们作了以下工做:

 

  1. QMetaObject::invokeMethod(object, "methodName",  
  2. Qt::QueuedConnection,  
  3. Q_ARG(type1, arg1),  
  4. 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相同

由于,发送者对象的线程依附性在这里可有可无。举例子说明

 

  1. class Thread : public QThread  
  2. {  
  3. Q_OBJECT  
  4. signals:  
  5. void aSignal();  
  6. protected:  
  7. void run() {  
  8. emit aSignal();  
  9. }  
  10. };  
  11. /* ... */  
  12. Thread thread;  
  13. Object obj;  
  14. QObject::connect(&thread, SIGNAL(aSignal()), &obj, SLOT(aSlot()));  
  15. thread.start();  

 

如上述代码,信号aSignal() 将在一个新的线程里被发射(由线程对象所表明);由于它并非Object 对象驻足的线程,因此尽管Thread对象thread与Object对象obj在同一个线程,但仍然是queued connection被使用。

译者注:这里做者分析的很透彻,但愿读者仔细揣摩Qt文档的这个错误。 也就是说 发送者对象自己在哪个线程对与信号槽链接类型不起任何做用,起到决定做用的是接收者对象所驻足的线程以及发射信号(该信号与接受者链接)的线程是否是在 同一个线程,本例中aSignal()在新的线程中被发射,因此采用queued connection)。

另一个常见的错误以下:

 

[c-sharp] view plain copy
  1. class Thread : public QThread  
  2. {  
  3. Q_OBJECT  
  4. slots:  
  5. void aSlot() {  
  6. /* ... */  
  7. }  
  8. protected:  
  9. void run() {  
  10. /* ... */  
  11. }  
  12. };  
  13. /* ... */  
  14. Thread thread;  
  15. Object obj;  
  16. QObject::connect(&obj, SIGNAL(aSignal()), &thread, SLOT(aSlot()));  
  17. thread.start();  
  18. 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()方法来改变它的线程依附性:

 

  1. class Worker : public QObject  
  2. {  
  3. Q_OBJECT  
  4. public slots:  
  5. void doWork() {  
  6. /* ... */  
  7. }  
  8. };  
  9. /* ... */  
  10. QThread thread;  
  11. Worker worker;  
  12. connect(obj, SIGNAL(workReady()), &worker, SLOT(doWork()));  
  13. worker.moveToThread(&thread);  
  14. thread.start();  

 

 

我应该何时使用线程

当你不得不使用一个阻塞式API时

当你须要(经过信号和槽,或者是事件、回调函数)使用一个没有提供非阻塞式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

计时器

这也许是线程滥用最坏的一种形式。若是咱们不得不重复调用一个方法(好比每秒),许多人会这样作:

 

  1. // 很是之错误  
  2. while (condition) {  
  3. doWork();  
  4. sleep(1); // this is sleep(3) from the C library  
  5. }  

 

而后他们发现这会阻塞事件循环,所以决定引入线程:

 

  1. // 错误  
  2. class Thread : public QThread {  
  3. protected:  
  4. void run() {  
  5. while (condition) {  
  6. // notice that "condition" may also need volatiness and mutex protection  
  7. // if we modify it from other threads (!)  
  8. doWork();  
  9. sleep(1); // this is QThread::sleep()  
  10. }  
  11. }  
  12. };  

 

一个更好也更简单的得到相同效果的方法是使用timers,即一个QTimer[doc.qt.nokia.com]对象,并设置一秒的超时时间,并让doWork方法成为它的槽:

 

  1. class Worker : public QObject  
  2. {  
  3. Q_OBJECT  
  4. public:  
  5. Worker() {  
  6. connect(&timer, SIGNAL(timeout()), this, SLOT(doWork()));  
  7. timer.start(1000);  
  8. }  
  9. private slots:  
  10. void doWork() {  
  11. /* ... */  
  12. }  
  13. private:  
  14. QTimer timer;  
  15. };  

 

全部咱们须要作的就是运行一个事件循环,而后doWork()方法将会被每隔秒钟调用一次。

网络/状态机

一个处理网络操做很是之常见的设计模式以下:

 

  1. socket->connect(host);  
  2. socket->waitForConnected();  
  3. data = getData();  
  4. socket->write(data);  
  5. socket->waitForBytesWritten();  
  6. socket->waitForReadyRead();  
  7. socket->read(response);  
  8. reply = process(response);  
  9. socket->write(reply);  
  10. socket->waitForBytesWritten();  
  11. /* ... and so on ... */  

 

不用多说,各类各样的waitFor*()函数阻塞了调用者使其没法返回到事件循环,UI被冻结等等。请注意上面的这段代码并无考虑到错误处理,不然它会更加地笨重。这个设计中很是错误的地方是咱们正在忘却网络编程是异步的设计,若是咱们构建一个同步的处理方法,则是本身给本身找麻烦。为了解决这个问题,许多人简单得将这些代码移到另外一个线程中。

另外一个更加抽象的例子:

 

  1. result = process_one_thing();  
  2. if (result->something())  
  3. process_this();  
  4. else  
  5. process_that();  
  6. wait_for_user_input();  
  7. input = read_user_input();  
  8. process_user_input(input);  
  9. /* ... */  

 

它多少反映了网络编程相同的陷阱。

 

让咱们回过头来从更高的角度来想一下咱们这里正在构建的代码:咱们想创造一个状态机,用以反映某类的输入并相对应的做某些动做。好比,上面的这段网络代码,咱们可能想作以下这些事情:

  • 空闲→ 正在链接 (当调用connectToHost());
  • 正在链接→ 已经链接(当connected() 信号被发射);
  • 已经链接→ 发送录入数据 (当咱们发送录入的数据给服务器);
  • 发送录入数据 → 录入 (服务器响应一个ACK)
  • 发送录入数据→ 录入错误(服务器响应一个NACK)

以此类推。

如今,有不少种方式来构建状态机(Qt甚至提供了QStateMachine[doc.qt.nokia.com]类),最简单的方式是用一个枚举值(及,一个整数)来记忆当前的状态。咱们能够这样重写如下上面的代码:

 

  1. class Object : public QObject  
  2. {  
  3. Q_OBJECT  
  4. enum State {  
  5. State1, State2, State3 /* and so on */  
  6. };  
  7. State state;  
  8. public:  
  9. Object() : state(State1)  
  10. {  
  11. connect(source, SIGNAL(ready()), this, SLOT(doWork()));  
  12. }  
  13. private slots:  
  14. void doWork() {  
  15. switch (state) {  
  16. case State1:  
  17. /* ... */  
  18. state = State2;  
  19. break;  
  20. case State2:  
  21. /* ... */  
  22. state = State3;  
  23. break;  
  24. /* etc. */  
  25. }  
  26. }  
  27. };  

 

那么“souce”对象和它的信号“ready()” 到底是什么? 咱们想让它们是什么就是什么:好比说,在这个例子中,咱们可能想把咱们的槽链接到socket的 QAbstractSocket::connected() 以及QIODevice::readyRead() 信号中,固然,咱们也能够简单地在咱们的用例中加更多的槽(好比一个槽用于处理错误状况,它将会被QAbstractSocket::error() 信号所通知)。这是一个真正的异步的,信号驱动的设计!

分解任务拆成不一样的块

假如咱们有一个开销很大的计算,它不可以轻易的移到另外一个线程中(或者说它根本不能被移动,举个例子,它必须运行在GUI线程中)。若是咱们能将计算拆分红小的块,咱们就能返回到事件循环,让它来派发事件,并让它激活处理下一个块相应的函数。若是咱们还记得queued connections是怎么实现的,那么会以为这是很容易可以作到的:一个事件派发到接收者所驻足的线程的事件循环;当事件被传递,相应的槽随之被激活。

咱们可使用特化QMetaObject::invokeMethod() 的激活类型为Qt::QueuedConnection 来获得相同的结果;这须要函数是可激活的。所以它须要一个槽或者用Q_INVOKABLE宏来标识。若是咱们同时想给函数中传入参数,他们须要使用Qt元 对象类型系统里的qRegisterMetaType()进行注册。请看下面这段代码:

 

  1. class Worker : public QObject  
  2. {  
  3. Q_OBJECT  
  4. public slots:  
  5. void startProcessing()  
  6. {  
  7. processItem(0);  
  8. }  
  9. void processItem(int index)  
  10. {  
  11. /* process items[index] ... */  
  12. if (index < numberOfItems)  
  13. QMetaObject::invokeMethod(this,  
  14. "processItem",  
  15. Qt::QueuedConnection,  
  16. Q_ARG(int, index + 1));  
  17. }  
  18. };  

 

 

由于并无引入多线程,因此暂停/进行/取消这样的计算并收集回结果变得简单。(结束

 

原文出处:

http://developer.qt.nokia.com/wiki/ThreadsEventsQObjects

请尊重原创做品和译文。转载请保持文章完整性,并以超连接形式注明原始做者主站点地址,方便其余朋友提问和指正。

转载于:https://www.cnblogs.com/hdjjun/p/3261984.html