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