Qt 学习之路:线程和 QObject

 

前面两个章节咱们从事件循环和线程类库两个角度阐述有关线程的问题。本章咱们将深刻线程间得交互,探讨线程和QObject之间的关系。在某种程度上,这才是多线程编程真正须要注意的问题。git

 

如今咱们已经讨论过事件循环。咱们说,每个 Qt 应用程序至少有一个事件循环,就是调用了QCoreApplication::exec()的那个事件循环。不过,QThread也能够开启事件循环。只不过这是一个受限于线程内部的事件循环。所以咱们将处于调用main()函数的那个线程,而且由QCoreApplication::exec()建立开启的那个事件循环成为主事件循环,或者直接叫主循环。注意,QCoreApplication::exec()只能在调用main()函数的线程调用。主循环所在的线程就是主线程,也被成为 GUI 线程,由于全部有关 GUI 的操做都必须在这个线程进行。QThread的局部事件循环则能够经过在QThread::run()中调用QThread::exec()开启:github

记得咱们前面介绍过,Qt 4.4 版本之后,QThread::run()再也不是纯虚函数,它会调用QThread::exec()函数。与QCoreApplication同样,QThread也有QThread::quit()QThread::exit()函数来终止事件循环。编程

线程的事件循环用于为线程中的全部QObjects对象分发事件;默认状况下,这些对象包括线程中建立的全部对象,或者是在别处建立完成后被移动到该线程的对象(咱们会在后面详细介绍“移动”这个问题)。咱们说,一个QObject的所依附的线程(thread affinity)是指它所在的那个线程。它一样适用于在QThread的构造函数中构建的对象:安全

在咱们建立了MyThread对象以后,objotherObjyetAnotherObj的线程依附性是怎样的?是否是就是MyThread所表示的那个线程?要回答这个问题,咱们必须看看到底是哪一个线程建立了它们:实际上,是调用了MyThread构造函数的线程建立了它们。所以,这些对象不在MyThread所表示的线程,而是在建立了MyThread的那个线程中。多线程

咱们能够经过调用QObject::thread()能够查询一个QObject的线程依附性。注意,在QCoreApplication对象以前建立的QObject没有所谓线程依附性,所以也就没有对象为其派发事件。也就是说,实际是QCoreApplication建立了表明主线程的QThread对象。并发

线程和QObject

咱们可使用线程安全的QCoreApplication::postEvent()函数向一个对象发送事件。它将把事件加入到对象所在的线程的事件队列中,所以,若是这个线程没有运行事件循环,这个事件也不会被派发。函数

值得注意的一点是,QObject及其全部子类都不是线程安全的(但都是可重入的)。所以,你不能有两个线程同时访问一个QObject对象,除非这个对象的内部数据都已经很好地序列化(例如为每一个数据访问加锁)。记住,在你从另外的线程访问一个对象时,它可能正在处理所在线程的事件循环派发的事件!基于一样的缘由,你也不能在另外的线程直接delete一个QObject对象,相反,你须要调用QObject::deleteLater()函数,这个函数会给对象所在线程发送一个删除的事件。post

此外,QWidget及其子类,以及全部其它 GUI 相关类(即使不是QObject的子类,例如QPixmap),甚至不是可重入的:它们只能在 GUI 线程访问。ui

QObject的线程依附性是能够改变的,方法是调用QObject::moveToThread()函数。该函数会改变一个对象及其全部子对象的线程依附性。因为QObject不是线程安全的,因此咱们只能在该对象所在线程上调用这个函数。也就是说,咱们只能在对象所在线程将这个对象移动到另外的线程,不能在另外的线程改变对象的线程依附性。还有一点是,Qt 要求QObject的全部子对象都必须和其父对象在同一线程。这意味着:this

  • 不能对有父对象(parent 属性)的对象使用QObject::moveToThread()函数
  • 不能在QThread中以这个QThread自己做为父对象建立对象,例如:

    这是由于QThread对象所依附的线程是建立它的那个线程,而不是它所表明的线程。

Qt 还要求,在表明一个线程的QThread对象销毁以前,全部在这个线程中的对象都必须先delete。要达到这一点并不困难:咱们只需在QThread::run()的栈上建立对象便可。

如今的问题是,既然线程建立的对象都只能在函数栈上,怎么能让这些对象与其它线程的对象通讯呢?Qt 提供了一个优雅清晰的解决方案:咱们在线程的事件队列中加入一个事件,而后在事件处理函数中调用咱们所关心的函数。显然这须要线程有一个事件循环。这种机制依赖于 moc 提供的反射:所以,只有信号、槽和使用Q_INVOKABLE宏标记的函数能够在另外的线程中调用。

QMetaObject::invokeMethod()静态函数会这样调用:

主意,上面函数调用中出现的参数类型都必须提供一个公有构造函数,一个公有的析构函数和一个公有的复制构造函数,而且要使用qRegisterMetaType()函数向 Qt 类型系统注册。

跨线程的信号槽也是相似的。当咱们将信号与槽链接起来时,QObject::connect()的最后一个参数将指定链接类型:

  • Qt::DirectConnection:直接链接意味着槽函数将在信号发出的线程直接调用
  • Qt::QueuedConnection:队列链接意味着向接受者所在线程发送一个事件,该线程的事件循环将得到这个事件,而后以后的某个时刻调用槽函数
  • Qt::BlockingQueuedConnection:阻塞的队列链接就像队列链接,可是发送者线程将会阻塞,直到接受者所在线程的事件循环得到这个事件,槽函数被调用以后,函数才会返回
  • Qt::AutoConnection:自动链接(默认)意味着若是接受者所在线程就是当前线程,则使用直接链接;不然将使用队列链接

注意在上面每种状况中,发送者所在线程都是可有可无的!在自动链接状况下,Qt 须要查看信号发出的线程是否是与接受者所在线程一致,来决定链接类型。注意,Qt 检查的是信号发出的线程,而不是信号发出的对象所在的线程!咱们能够看看下面的代码:

aSignal()信号在一个新的线程被发出(也就是Thread所表明的线程)。注意,由于这个线程并非Object所在的线程(Object所在的线程和Thread所在的是同一个线程,回忆下,信号槽的链接方式与发送者所在线程无关),因此这里将会使用队列链接。

另一个常见的错误是:

这里的obj发出aSignal()信号时,使用哪一种链接方式?答案是:直接链接。由于Thread对象所在线程发出了信号,也就是信号发出的线程与接受者是同一个。在aSlot()槽函数中,咱们能够直接访问Thread的某些成员变量,可是注意,在咱们访问这些成员变量时,Thread::run()函数可能也在访问!这意味着两者并发进行:这是一个完美的致使崩溃的隐藏bug。

另一个例子可能更为重要:

这个例子也会使用队列链接。然而,这个例子比上面的例子更具隐蔽性:在这个例子中,你可能会以为,Object所在Thread所表明的线程中被建立,又是访问的Thread本身的成员数据。稍有不慎便会写出这种代码。

为了解决这个问题,咱们能够这么作:Thread构造函数中增长一个函数调用:moveToThread(this)

实际上,这的确可行(由于Thread的线程依附性被改变了:它所在的线程成了本身),可是这并非一个好主意。这种代码意味着咱们其实误解了线程对象(QThread子类)的设计意图:QThread对象不是线程自己,它们实际上是用于管理它所表明的线程的对象。所以,它们应该在另外的线程被使用(一般就是它本身所在的线程),而不是在本身所表明的线程中。

上面问题的最好的解决方案是,将处理任务的部分与管理线程的部分分离。简单来讲,咱们能够利用一个QObject的子类,使用QObject::moveToThread()改变其线程依附性:

相关文章
相关标签/搜索