线程和 QObject
QThread 继承自 QObject。它发出信号以指示线程开始或完成执行,并提供了一些槽。
更有趣的是,QObject 可以在多个线程中使用,发出调用其他线程槽的信号,并将事件发布给其他线程中的对象。这是可能的,因为每个线程都被允许有自己的事件循环。
QObject 重入性
QObject 是重入的。它的大多数非 GUI 子类,例如 QTimer、QTcpSocket、QUdpSocket 和 QProcess,也是重入的,这使得可以从多个线程同时使用这些类。请注意,这些类被设计为在单个线程中创建和使用;在一个线程中创建对象然后从另一个线程调用其函数是不保证能工作的。有三个约束需要注意
- QObject 的子代必须在创建父类的线程中创建。 这意味着,在其他事情中,你不应该将 QThread 对象 (
this
) 作为在线程中创建的对象的父级(因为 QThread 对象自身是在另一个线程中创建的)。 - 事件驱动对象只能在单一线程中使用。 具体来说,这适用于 计时器机制 和 网络模块。例如,你无法在不是 对象线程 的线程中启动计时器或连接套接字。
- 你必须确保在销毁 QThread 之前已经删除了线程中创建的所有对象。 这可以通过在你的 run() 实现中在堆栈上创建对象来完成。
虽然 QObject 是重入的,但 GUI 类,尤其是 QWidget 及其所有子类,都不是重入的。它们只能从主线程使用。如前所述,QCoreApplication::exec() 也必须从那个线程调用。
在实践中,可以通过将耗时的操作放在单独的工作线程中,并在工作线程完成后在主线程中显示结果来轻松解决只能在主线程中使用 GUI 类的问题。这是 Mandelbrot 示例和 Blocking Fortune Client 示例所采用的方法。
通常情况下,在创建QApplication之前创建QObject不被支持,并且可能会在不同平台上导致退出时出现奇怪的崩溃。这意味着也不支持QObject的静态实例。一个结构良好的单线程或多线程应用程序应该将QApplication作为第一个创建,最后一个销毁的QObject。
每个线程可以有自己的事件循环
每个线程都可以有自己的事件循环。初始线程可以通过调用QCoreApplication::exec()来启动其事件循环,或者在单对话框GUI应用程序中,有时调用QDialog::exec()。其他线程可以使用QThread::exec()启动事件循环。和QCoreApplication一样,QThread提供了一个exit(int)函数和一个quit()槽。
线程中的事件循环使得该线程可以使用某些需要事件循环存在的非GUI Qt类(如QTimer、QTcpSocket和QProcess)。这也使得将任何线程的信号连接到特定线程的槽成为可能。这将在下面的跨线程的信号和槽部分中详细说明。
如果一个QObject实例存在于创建它的线程中,那么可以说该QObject实例活着。该对象的事件由该线程的事件循环传达。可以使用QObject::thread()获取一个QObject存在的线程。
函数QObject::moveToThread()更改对象及其子对象(如果对象有父对象则不能移动)的线程亲和性。
如果一个线程在它所拥有的对象的线程之外调用QObject上的delete
(或以其他方式访问该对象),是不安全的,除非你保证该对象此刻没有处理事件。应该使用QObject::deleteLater()代替,这将发布一个DeferredDelete事件,该事件将被对象线程的事件循环最终拾取。默认情况下,拥有QObject的线程是创建QObject的线程,但不是在调用QObject::moveToThread()后。
如果没有事件循环正在运行,事件就不会发送到对象。例如,如果你在线程中创建QTimer对象但从不调用exec(),该QTimer将永远不会发出它的timeout()信号。调用deleteLater()也不会起作用。(这些限制也适用于主线程。)
您可以使用线程安全的函数QCoreApplication::postEvent在任何时间手动将事件发到任何线程中的任何对象。这些事件将自动由创建该对象的线程的事件循环传输。
事件过滤器在所有线程中都受到支持,但有限制,即监视对象必须生活在被监视对象相同的线程中。类似地,QCoreApplication::sendEvent(与postEvent不同)只能用于将事件调度到调用该函数的线程中存在的对象。
从其他线程访问QObject子类
QObject及其所有子类都不是线程安全的。这包括整个事件传递系统。需要记住的是,在您从其他线程访问对象时,事件循环可能正在向您的QObject子类传递事件。
如果您在一个不在当前线程中的QObject子类上调用函数,且对象可能会接收事件,您必须使用互斥锁来保护对QObject子类内部数据的所有访问;否则,您可能会遇到崩溃或其他不良行为。
与其他对象一样,QThread对象居住在对象创建的线程中——而非调用QThread::run()函数时创建的线程。通常情况下,在您的QThread子类中提供槽函数是不安全的,除非您使用互斥锁保护成员变量。
另一方面,您可以从您的QThread::run()实现中安全地发射信号,因为信号发射是线程安全的。
跨线程的信号和槽
Qt支持以下信号-槽连接类型
- 自动连接(默认)如果信号是在接收对象具有关联性的线程中发射的,则行为与直接连接相同。否则,行为与排队连接相同。
- 直接连接槽会在信号发射时立即被调用。槽在发射者的线程中执行,这不一定与接收者的线程相同。
- 排队连接槽在控制返回到接收者线程的事件循环时被调用。槽在接收者线程中执行。
- 阻塞排队连接槽的调用方式与排队连接相同,但当前线程会阻塞,直到槽返回。
注意:使用此类型在同一线程中连接对象会导致死锁。
- 唯一连接该行为与自动连接相同,但仅在不会重复现有连接的情况下才会建立连接。即,如果相同的信号已经连接到同一槽的同一对对象,则不会建立连接,并且connect()返回
false
。
可以通过传递额外的参数到connect()来指定连接类型。请注意,当收发者和接收者位于不同的线程时,如果接收者线程中正在运行事件循环,则使用直接连接是不安全的,与在另一直线上的对象上调用任何函数一样不安全。
QObject::connect()本身是线程安全的。
《Mandelbrot》示例使用排队连接在工作线程和主线程之间进行通信。为了避免冻结主线程的事件循环(从而冻结应用程序的用户界面),所有Mandelbrot分形计算都是在单独的工作线程中完成的。当线程完成渲染分形时,会发射一个信号。
类似地,《Blocking Fortune Client》示例使用单独的线程来异步地与TCP服务器进行通信。
© 2024 The Qt Company Ltd。本文档中包含的贡献内容均为各自所有者的版权。本提供的文档根据自由软件基金会的发布,受GNU自由文档许可证第1.3版 的条款许可。Qt及其相关标志是世界范围内芬兰和/或其他国家的The Qt Company Ltd.的商业标志。所有其他商标均为各自所有者的财产。