线程和 QObject

QThread 继承 QObject 。它发射指示线程启动 (或执行完成) 的信号,且还提供了几个槽。

更有趣的是 QObject 可以用于多线程,发射援引其它线程槽的信号,并把事件张贴给 "存活" 于其它线程中的对象。这是可能的,因为每个线程都允许拥有它自己的事件循环。

QObject 重入

QObject 可重入。它的大多数非 GUI (图形用户界面) 子类,譬如 QTimer , QTcpSocket , QUdpSocket and QProcess ,也可重入,使之可能从多线程同时使用这些类。注意,设计是从单线程创建和使用这些类;在某个线程中创建对象并从另一线程调用其函数,不能保证能工作。要注意存在 3 个约束:

  • 子级对于 QObject 必须始终在创建父级的线程中创建。 这隐含,除其它事情外,从不应该传递 QThread 对象 ( this ) 作为在线程中创建对象的父级 (由于 QThread 对象自身是在另一线程中创建的)。
  • 事件驱动对象只可以用于单线程。 具体来说,这适用于 计时器机制 网络模块 。例如,无法启动计时器 (或连接套接字) 当所在的线程不是 对象的线程 .
  • 必须确保删除线程中创建的所有对象,先于删除 QThread . 可以轻松做到这,通过创建堆栈对象在 run() 实现。

尽管 QObject 是可重入 GUI 类,显而易见 QWidget 及其所有子类,都不可重入。只可以从主线程使用它们。如前所述, QCoreApplication::exec () 还必须从该线程调用。

在实践中,不可能在主线程外的其它线程中使用 GUI (图形用户界面) 类,通过把耗时操作放入单独工作者线程,并在工作线程完成时在主线程屏幕中显示结果,可轻松解决。此实现方式可以用于 Mandelbrot 范例 阻塞 Fortune 客户端范例 .

一般而言,创建 QObject 先于 QApplication 不支持,且退出时可能导致奇怪崩溃,从属平台。这意味着静态实例 QObject 也不支持。结构合理的单 (或多) 线程应用程序应该使 QApplication 被首先创造,且最后销毁 QObject .

每线程事件循环

每个线程都可以拥有它自己的事件循环。初始线程启动其事件循环是使用 QCoreApplication::exec (),或对于单对话框 GUI (图形用户界面) 应用程序,有时是 QDialog::exec ()。其它线程启动事件循环可以使用 QThread::exec ()。像 QCoreApplication , QThread 提供 exit (int) 函数和 quit() 槽。

线程中的事件循环使之可能对要使用某些非 GUI Qt 类的线程,要求存在事件循环 (譬如 QTimer , QTcpSocket ,和 QProcess )。还使之可能把来自任何线程的信号,连接到特定线程槽。阐述这的更多细节在 信号和槽跨线程 以下章节。

Threads, objects, and event loops

A QObject 实例据称是 live 在创建它的线程中。此对象的事件由该线程的事件循环分派。线程对于 QObject 存活的获得是使用 QObject::thread ().

The QObject::moveToThread () 函数改变对象,及其子级的线程亲缘关系 (对象无法移动,若它拥有父级)。

调用 delete QObject 从线程而不是某个线程其 owns 对象 (或以其它方式访问对象) 是不安全的,除非保证对象在那刻不处理事件。使用 QObject::deleteLater () 代替,和 DeferredDelete 事件将被张贴,最终将拾取对象线程的事件循环。默认情况下,线程 owns a QObject 是线程 creates the QObject ,但不后于 QObject::moveToThread () 被调用。

若没有事件循环在运行,就不会将事件交付给对象。例如,若创建 QTimer 对象在线程,但从不调用 exec() QTimer 将从不发射其 timeout() 信号。调用 deleteLater() 也不会工作 (这些限定也适用于主线程)。

可以在任何时间手动将事件张贴给任何线程中的任何对象,使用线程安全函数 QCoreApplication::postEvent ()。将通过创建对象线程的事件循环,自动分派事件。

支持事件过滤器的所有线程,具有监视对象必须活在如被监视对象的同一线程中的限定。同样, QCoreApplication::sendEvent () (不像 postEvent() ) 只可以用于把事件分派给活在从那里调用函数的线程的对象。

从其它线程访问 QObject 子类

QObject 及其所有子类都不是线程安全的。这包括整个事件交付系统。它很重要,记住事件循环可能把事件交付给 QObject 子类,当从另一线程访问对象时。

若正调用函数在 QObject 子类未活在当前线程中且对象可能接收事件,就必须保护所有访问对 QObject 子类的内部数据按互斥;否则,可能经历崩溃 (或其它不期望行为)。

像其它对象, QThread 对象活在创建对象的线程中 -- not 在创建线程中,当 QThread::run () 被调用。提供槽通常不安全在 QThread 子类,除非采用互斥保护成员变量。

另一方面,可以安全地发射信号从 QThread::run () 实现,因为信号发出是线程安全的。

信号和槽跨线程

Qt 支持这些信号/槽连接类型:

  • 自动连接 (默认) 若在拥有亲缘关系的接收对象的线程中发射信号,那么行为如同直接连接。否则,行为如同队列连接。
  • 直接连接 立即援引槽,当发射信号时。在发射器线程中执行槽,不必是接收者线程。
  • 队列连接 槽被援引,当控制返回给接收者线程的事件循环时。在接收者线程中执行槽。
  • 阻塞队列连接 如同队列连接援引槽,除当前线程阻塞,直到槽返回外。

    注意: 使用这种类型连接同一线程中的对象,会导致死锁。

  • 唯一连接 行为如同 "自动连接",但才做出连接,若它不复制现有连接时。即,若同一信号已连接到用于同一对对象的同一槽,那么不会做出连接且 connect() 返回 false .

可以指定连接类型,通过把额外自变量传递给 connect() 。要意识到,当发送者和接收者活在不同线程中时 (若事件循环运行在接收者线程中) 使用直接连接不安全,出于相同原因,调用活在另一线程中对象的任何函数也不安全。

QObject::connect () 本身是线程安全的。

The Mandelbrot 范例 使用 "队列连接" 在工作者线程和主线程之间,进行通信。为避免冻结主线程的事件循环 (并因此,冻结应用程序的用户界面),所有 Mandelbrot 分形计算都是在单独工作者线程中完成。线程发射信号,当分形渲染完成时。

同样, 阻塞 Fortune 客户端范例 使用单独线程与 TCP 服务器进行异步通信。