Threads and QObjects
QThread 继承自 QObject. 它发出信号指示线程开始或完成执行, 并提供一些槽函数.
更有趣的是, QObject 可以在多个线程中使用, 发出调用其他线程中的槽函数的信号, 并将事件发送到 "生存" 在其他线程中的对象. 这是可能的, 因为每个线程都可以有自己的事件循环.
QObject Reentrancy
QObject 是可重入的. 它的大多数非 GUI 子类, 如 QTimer, QTcpSocket, QUdpSocket 和 QProcess, 也是可重入的, 使得可以同时从多个线程使用这些类. 注意, 这些类被设计为在单个线程中创建和使用; 在一个线程中创建对象并从另一个线程调用其函数并不能保证一定有效. 需要注意三个限制:
- QObject 的子对象必须始终在创建父对象的线程中创建. 这意味着, 除非必要, 你不应该将 QThread 对象 (
this
) 作为在线程中创建的对象的父级传递 (因为 QThread 对象本身是在另一个线程中创建的). - 事件驱动对象只能在单个线程中使用. 具体来说, 这适用于 定时器机制 和 网络模块. 例如, 你不能在非对象线程的线程中启动定时器或连接套接字.
- 在删除 QThread 之前, 必须确保删除线程中创建的所有对象. 这可以通过在 run() 实现中在堆栈上创建对象来轻松完成.
尽管 QObject 是可重入的, 但 GUI 类 ( 尤其是 QWidget 及其所有子类) 是不可重入的. 它们只能在主线程中使用. 如前所述, 还必须从该线程调用 QCoreApplication::exec().
实际上, 无法在主线程以外的其他线程中使用 GUI 类的问题可以通过将耗时的操作放在单独的工作线程中并在工作线程完成时将结果显示在主线程的屏幕上来轻松解决. 这是用于实现 Mandelbrot Example 和 Blocking Fortune Client Example 的方法.
一般来说, 不支持在 QApplication 之前创建 QObjects, 并且可能会导致退出时出现奇怪的崩溃, 具体取决于平台. 这意味着 QObject 的静态实例也不被支持. 一个结构正确的单线程或多线程应用程序应该使 QApplication 成为第一个创建, 最后销毁的 QObject.
Per-Thread Event Loop
每个线程都可以有自己的事件循环. 初始线程使用 QCoreApplication::exec() 启动其事件循环, 或者对于单对话框 GUI 应用程序, 有时使用 QDialog::exec(). 其他线程可以使用 QThread::exec() 启动事件循环. 与 QCoreApplication 一样, QThread 提供 exit(int) 函数和 quit() 槽函数.
线程中的事件循环使线程可以使用某些需要事件循环的非 GUI Qt 类 (如 QTimer, QTcpSocket, QProcess). 它还使得将来自任何线程的信号连接到特定线程的槽函数成为可能. 下面的 跨线程信号和槽 部分对此进行了更详细的解释.
QObject 实例存在于创建它的线程中. 该对象的事件由该线程的事件循环调度. QObject 所在的线程可以使用 QObject::thread() 获取.
QObject::moveToThread() 函数更改对象及其子对象的线程关联性 (如果对象有父对象, 则无法移动该对象).
从拥有该对象的线程以外的线程调用 QObject 的 delete
(或以其他方式访问该对象) 是不安全的, 除非你确保该对象当时不处理事件. 使用 QObject::deleteLater() 代替, 将发布 DeferredDelete 事件, 对象线程的事件循环最终将拾取该事件. 默认情况下, 拥有 QObject 的线程是创建 QObject 的线程, 但在调用 QObject::moveToThread() 之后就不是了.
如果没有事件循环正在运行, 事件将不会传递到对象. 例如, 如果你在线程中创建 QTimer 对象但从未调用 exec(), 则 QTimer 将永远不会发出 timeout() 信号. 调用 deleteLater() 也不起作用. (这些限制也适用于主线程.)
你可以随时使用线程安全函数 QCoreApplication::postEvent() 手动将事件发布到任何线程中的任何对象. 事件将由创建对象的线程的事件循环自动调度.
所有线程都支持事件过滤器, 但限制是监视对象必须与被监视对象位于同一线程中. 类似地, QCoreApplication::sendEvent() (不像 postEvent()) 只能用于将事件分派给调用该函数的线程中的对象.
Accessing QObject Subclasses from Other Threads
QObject 及其所有子类都不是线程安全的. 这包括整个事件传递系统. 重要的是要记住, 当你从另一个线程访问对象时, 事件循环可能会向你的 QObject 子类传递事件.
如果你正在调用不在当前线程中的 QObject 子类上的函数, 并且该对象可能会接收事件, 则必须使用互斥体保护对 QObject 子类内部数据的所有访问; 否则, 你可能会遇到崩溃或其他不良行为.
与其他对象一样, QThread 对象存在于创建该对象的线程中 -- 而不是存在于调用 QThread::run() 时创建的线程中. 在 QThread 子类中提供槽函数通常是不安全的, 除非你使用互斥体保护成员变量.
另一方面, 你可以安全地从 QThread::run() 实现中发出信号, 因为信号发出是线程安全的.
Signals and Slots Across Threads
Qt 支持以下信号槽连接类型:
- Auto Connection (默认) 如果信号在接收对象具有亲和力的线程(同一线程)中发出, 则行为与直接连接相同. 否则, 行为与 Direct Connection 相同."
- Direct Connection 当信号发出时, 槽会立即被调用. 槽函数在发送对象的线程中执行, 该线程不一定是接收对象的线程.
- Queued Connection 当控制返回到接收者线程的事件循环时, 将调用该槽函数 槽函数在接收者的线程中执行.
- Blocking Queued Connection 槽函数的调用方式与队列连接相同, 但当前线程会阻塞, 直到槽函数返回.
注意: 使用该类型连接同一个线程中的对象会导致死锁.
- Unique Connection 与自动连接相同, 但仅当不存在重复现有连接时才会建立连接. 即., 如果相同的信号已经连接到同一对象的相同槽函数, 则不会建立连接, 且 connect() 返回
false
.
你可以在调用 connect() 传递附加参数来指定连接类型. 注意, 如果事件循环在接收者的线程中运行, 则当发送者和接收者位于不同线程中时使用直接连接是不安全的, 这与调用位于另一个线程中的对象上的任何函数是不安全的原因相同.
QObject::connect() 本身是线程安全的.
Mandelbrot Example 示例使用队列连接在工作线程和主线程之间进行通信. 为了避免阻塞主线程的事件循环 (以及应用程序的用户界面), 所有 Mandelbrot 分形计算都在单独的工作线程中完成. 当完成分形渲染时, 线程会发出一个信号.
类似地, Blocking Fortune Client Example 示例使用单独的线程与 TCP 服务器异步通信.