原文地址 Threads and QObjects | Qt 6.5
QThread 继承了QObject。它发出信号以指示线程开始或完成执行,并提供一些槽函数。
更有趣的是,QObjects 可以在多个线程中使用,发出调用其他线程中的槽的信号,并将事件发布到“存在”在其他线程中的对象。这是可能的,因为允许每个线程有自己的事件循环。
QObject 重入
QObject 是可重入的。 它的大多数非 GUI 子类,例如 QTimer、QTcpSocket、QUdpSocket 和 QProcess,也是可重入的,从而可以从多个线程同时使用这些类。 请注意,这些类被设计为在单个线程中创建和使用; 不能保证在一个线程中创建一个对象并从另一个线程调用它的函数。 需要注意三个约束:
-
QObject 的子对象必须始终在创建父对象的线程中创建。 这意味着,除其他事项外,您永远不应将 QThread 对象 (this) 作为在线程中创建的对象的父对象传递(因为 QThread 对象本身是在另一个线程中创建的)。
-
事件驱动对象只能在单个线程中使用。具体来说,这适用于定时器机制和网络模块。例如,您不能在不是对象线程的线程中启动计时器或连接套接字。
-
您必须确保在删除 QThread 之前删除线程中创建的所有对象。这可以通过在 run() 实现中在堆栈上创建对象来轻松完成。
尽管 QObject 是可重入的,但 GUI 类,尤其是 QWidget 及其所有子类,都不是可重入的。它们只能在主线程中使用。如前所述,还必须从该线程调用 QCoreApplication::exec()。
实际上,通过将耗时的操作放在单独的工作线程中并在工作线程完成时在主线程的屏幕上显示结果,可以很容易地解决无法在主线程以外的其他线程中使用 GUI 类的问题。
通常,不支持在 QApplication 之前创建 QObjects,这可能会导致退出时发生奇怪的崩溃,具体取决于平台。 这意味着也不支持 QObject 的静态实例。 一个结构合理的单线程或多线程应用程序应该使 QApplication 最先创建,最后销毁 QObject。
线程事件循环
每个线程都可以有自己的事件循环。 初始线程使用 QCoreApplication::exec() 启动其事件循环,或者对于单对话框 GUI 应用程序,有时使用 QDialog::exec()。 其他线程可以使用 QThread::exec() 启动事件循环。 与 QCoreApplication 一样,QThread 提供了一个 exit(int) 函数和一个 quit() 槽。
线程中的事件循环使线程可以使用某些需要事件循环的非 GUI Qt 类(例如 QTimer、QTcpSocket 和 QProcess)。 它还可以将来自任何线程的信号连接到特定线程的槽。 这在下面的跨线程信号和槽部分中有更详细的解释。
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 支持这些信号槽连接类型:
-
Auto Connection 自动连接(默认),如果信号在接收对象具有亲和力的线程中发出,则行为与直接连接相同。否则,行为与排队连接相同。”
-
Direct Connection 直接连接,当信号发出时,槽函数会立即被调用。槽函数在信号发送者的线程中执行,不一定是接收者的线程。
-
Queued Connection 排队连接,当控制权返回到接收方线程的事件循环时调用槽。 槽函数在接收方的线程中执行。信号发送者发送信号后立即返回,相当于异步调用
-
Blocking Queued Connection 阻塞排队连接,槽函数在接收方的线程中执行。信号发送者等待槽函数执行结束,相当于同步调用。注意:使用该类型连接同一个线程中的对象会造成死锁。
-
Unique Connection 唯一连接,该行为与自动连接相同,但只有在不复制现有连接时才会建立连接。 也就是说,如果相同的信号已经连接到同一对对象的相同插槽,则不会建立连接并且 connect() 返回 false。注意:Qt::UniqueConnections 不适用于 lambda、非成员函数和仿函数;它们仅适用于连接到成员函数。
可以通过将附加参数传递给 connect() 来指定连接类型。 请注意,如果事件循环在接收方的线程中运行,那么在发送方和接收方位于不同线程中时使用直接连接是不安全的,原因与调用位于另一个线程中的对象上的任何函数是不安全的原因相同。
QObject::connect() 本身是线程安全的。