Qt 的线程与事件循环

来源:互联网 发布:宇宙射线知乎 编辑:程序博客网 时间:2024/05/18 04:56

目录(?)[-]

  1. 起源
  2. QThread 的两种使用方法
  3. 另外
  4. QThreadrun
  5. QObjectconnect
  6. 怎么办呢
  7. 主线程信号QThread槽
  8. run中信号与QThread中槽
  9. 推荐的方法
  10. 其他
  11. 参看

        周末天冷,索性把电脑抱到床上上网,这几天看了 dbzhang800 博客关于 Qt 事件循环的几篇 Blog,发现自己对 Qt 的事件循环有不少误解。从来只看到现象,这次借 dbzhang800 的博客,就代码论事,因此了解到一些 Qt 深层的实现,虽然是在 Qt 庞大的构架里只算的是冰山的一角,确让人颇为收益。

        从 dbzhang800 的博客中转载两篇关于事件循环的文章,放在一起,写作备忘。

        再次提到的一点是:事件循环和线程没有必然关系。 QThread 的 run() 方法始终是在一个单独线程执行的,但只有在 run() 方法中使用了 exec() 才真正开启了一个单独的事件循环。

        顺便一提,模态对话框和事件循环也没有必然关系,可以转读这篇文章 QDialog 模态对话框与事件循环

         以下内容转载自 dbzhang800 的博客:                                                                               

         「QThread 的使用方法」                                                                                                      
          原文地址:http://hi.baidu.com/cyclone/blog/item/5fac3bc7ab1b90d1d10060f2.html  

         「QThread 使用探讨」                                                                                                          
          原文地址:http://hi.baidu.com/cyclone/blog/item/a33794ee00acba262cf53442.html 


QThread 的使用方法

起源

        昨天不小心看到Qt开发人员( Bradley T. Hughes)Blog中的一片文章 you are-doing-it-wrong 。 结果看得头昏脑胀:好歹也自学了近1年的Qt,也一直很小心、很认真地阅读Qt和manual和例子等资料,却被突然告知,QThread的正确使用方法是一种自己从没见过,而且Qt manual、example、书籍中都没有提到过的一种方法。到底怎么了... 

        莫非manual、exmaple以及资料中的介绍都是错的??

        认真看看其他的人的评论,总算理清了一点头绪。所有事情源于 QThread 的事件循环!

QThread 的两种使用方法

1. 不使用事件循环。这是官方的 Manual 、example 以及相关书籍中都介绍的一种的方法。

a. 子类化 QThread

b. 重载 run 函数,run函数内有一个 while 或 for 的死循环

c. 设置一个标记为来控制死循环的退出。

2. 使用事件循环。(博客 you are-doing-it-wrong 批驳的就是这种情况下的 一种用法。)

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推荐的方法。

        终于看懂了,但不管怎么说,都应该是 QThread 当初的设计导致的这种问题,而所有文档和例子中都没有提到该如何使用Qthread 进一步加剧了对QThread的这种误用。


QThread 使用探讨

        QThread 似乎是很难的一个东西,特别是信号和槽,有非常多的人(尽管使用者本人往往不知道)在用不恰当(甚至错误)的方式在使用 QThread,随便用google一搜,就能搜出大量结果出来。无怪乎Qt的开发人员 Bradley T. Hughes 声嘶力竭地喊you are-doing-it-wrong。

        和众多用户一样,初次看到这个时,感到 Bradley T. Hughes有 些莫名奇妙,小题大作。尽管不舒服,当时还是整理过一篇博客QThread 的使用方法

        时间过去3个月,尽管依然没怎么用thread;但今天csdn论坛中有人问到这个问题,想想还是尽我所能整理一下吧。提升自己,方便他人,何乐而不为呢?

        QThread东西还是比较多的,而且我对底层对象了解有限,仅就一点进行展开(或许是大家最关心的一点):QThread中的slots在那个线程中执行?

QThread::run

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.

这么短的文字一眼就看完了,可是,这是什么意思呢?又能说明什么问题呢?看段简单代码:

class Thread:public QThread {     Q_OBJECT public:     Thread(QObject* parent=0):QThread(parent){} public slots:     void slot() { ... } signals:     void sig(); protected:     void run() { ...} }; int main(int argc, char** argv) { ...     Thread thread; ... }

对照前面的定理,run函数中的代码时确定无疑要在次线程中运行的,那么其他的呢?比如 slot 是在次线程还是主线程中运行?

你想说主线程,但又心有不甘,对么?

QObject::connect

        涉及信号槽,我们就躲不过 connect 函数,只是这个函数大家太熟悉。我不好意思再用一堆废话来描述它,但不说又不行,那么折中一下,只看它的最后一个参数吧(为了简单起见,只看它最常用的3个值)

        下面的列表,我们暂称为定理二:

  • 自动连接(Auto Connection)
    • 这是默认设置
    • 如果信号在接收者所依附的线程内发射,则等同于直接连接
    • 如果发射信号的线程和接受者所依附的线程不同,则等同于队列连接
    • 也就是这说,只存在下面两种情况
  • 直接连接(Direct Connection)
    • 当信号发射时,槽函数将直接被调用。
    • 无论槽函数所属对象在哪个线程,槽函数都在发射信号的线程内执行。
  • 队列连接(Queued Connection)
    • 当控制权回到接受者所依附线程的事件循环时,槽函数被调用。
    • 槽函数在接收者所依附线程执行。

        同前面一样,这些文字大家都能看懂。但含义呢?

        不妨继续拿前面的例子来看,slot 函数是在主线程还是次线程中执行呢?

        定理二强调两个概念:发送信号的线程 和 接收者所依附的线程。而 slot 函数属于我们在main中创建的对象 thread,即thread依附于主线程

  • 队列连接告诉我们:槽函数在接受者所依附线程执行。即 slot 将在主线程执行
  • 直接连接告诉我们:槽函数在发送信号的线程执行。信号在那个线程发送呢??不定!
  • 自动连接告诉我们:二者不同,等同于队列连接。即 slot 在主线程执行

        太绕了?不是么(要彻底理解这几句话,你可能需要看Qt meta-object系统和Qt event系统)

怎么办呢?

        如果上两节看不懂,就记住下面的话吧(自己总结的,用词上估计会不太准确)。

  • QThread 是用来管理线程的,它所依附的线程和它管理的线程并不是同一个东西
  • QThread 所依附的线程,就是执行 QThread t(0) 或 QThread * t=new QThread(0) 的线程。也就是咱们这儿的主线程
  • QThread 管理的线程,就是 run 启动的线程。也就是次线程
  • 因为QThread的对象依附在主线程中,所以他的slot函数会在主线程中执行,而不是次线程。除非:
    • QThread 对象依附到次线程中(通过movetoThread)
    • slot 和信号是直接连接,且信号在次线程中发射
  • 但上两种解决方法都不好,因为QThread不是这么用的(Bradley T. Hughes)

        好了,不再添加更多文字了,看代码,估计咱们都会轻松点

主线程(信号)QThread(槽)

        这是 Qt Manual 和 例子中普遍采用的方法。 但由于manual没说槽函数是在主线程执行的,所以不少人都认为它应该是在次线程执行了。

  • 定义一个 Dummy 类,用来发信号
  • 定义一个 Thread 类,用来接收信号
    • 重载 run 函数,目的是打印 threadid
/*!* \file main.cpp** Copyright (C) 2010, dbzhang800* All rights reserved.**/#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(); }

然后看到结果(具体值每次都变,但结论不变)

main thread: 0x1a40 from thread slot_main: 0x1a40 thread thread: 0x1a48

看到了吧,槽函数的线程和主线程是一样的!

如果你看过Qt自带的例子,你会发现 QThread 中 slot 和 run 函数共同操作的对象,都会用QMutex锁住。为什么?

因为slot和run处于不同线程,需要线程间的同步!

如果想让槽函数slot在次线程运行(比如它执行耗时的操作,会让主线程死掉),怎么解决呢?

  • 注意:dummy信号是在主线程发射的, 接收者 thread 也在主线程中。
  • 参考我们前面的结论,很容易想到:
    • 将 thread 依附的线程改为次线程不就行了?
    • 这也是代码中注释掉的 moveToThread(this)所做的,去掉注释,你会发现slot在次线程中运行

main thread: 0x13c0 thread thread: 0x1de0 from thread slot_main: 0x1de0

这可以工作,但这是 Bradley T. Hughes 强烈批判的用法。推荐的方法后面会给出。

run中信号与QThread中槽

  • 定义一个 Dummy 类,在run中发射它的信号
    • 也可以在run中发射 Thread 类中的信号,而不是Dummy(效果完全一样)
  • QThread 定义槽函数,重载run函数
/*!* \file main.cpp** Copyright (C) 2010, dbzhang800* All rights reserved.**/#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;         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
  • 其实没悬念,肯定是主线程
    • thread 对象本身在主线程。所以它的槽也在要在主线程执行

如何解决呢?

  • (方法一)前面提了 moveToThread,这儿可以用,而且可以解决问题。当同样,是被批判的对象。
  • (方法二)注意哦,这儿我们的信号时次线程发出的,对比connect连接方式,会发现:
    • 采用直接连接,槽函数将在次线程(信号发出的线程)执行
    • 这个方法不太好,因为你需要处理slot和它的对象所在线程的同步。需要 QMutex 一类的东西

推荐的方法

        千呼万唤始出来。

        其实,这个方法太简单,太好用了。定义一个普通的QObject派生类,然后将其对象move到QThread中。使用信号和槽时根本不用考虑多线程的存在。也不用使用QMutex来进行同步,Qt的事件循环会自己自动处理好这个。

/*!* \file main.cpp** Copyright (C) 2010, dbzhang800* All rights reserved.**/#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(); }

        结果:恩,slot确实不在主线程中运行(这么简单不值得欢呼么?)

main thread: 0x1a5c from thread slot: 0x186c

其他

  • 本文只考虑了使用事件循环的情况,也有可能run中没有事件循环。这时信号与槽会与本文有点差别。比如run中使用connect时,队列连接就受限制了。其实只要理解了前面这些,没有事件循环的情况很容易就想通了。

参看

  • http://labs.qt.nokia.com/blogs/2010/06/17/youre-doing-it-wrong/
  • http://labs.qt.nokia.com/blogs/2010/06/17/youre-doing-it-wrong/
  • http://labs.qt.nokia.com/blogs/2006/12/04/threading-without-the-headache/
  • http://labs.qt.nokia.com/blogs/2007/07/05/qthreads-no-longer-abstract/
  • http://gitorious.org/qthreadhowto/qthreadhowto/trees/master
  • http://blog.exys.org/entries/2010/QThread_affinity.html
  • http://thesmithfam.org/blog/2010/02/07/talking-to-qt-threads/
  • http://doc.qt.nokia.com/4.7/threads-starting.html
  • http://doc.qt.nokia.com/4.7/threads-qobject.html
  • http://doc.qt.nokia.com/4.7/qthread.html