穿线基础知识

什么是线程?

线程和进程一样,都是并行做事的。那么线程与进程有什么不同呢?当你在电子表格上进行计算时,同一桌面上可能还有一个媒体播放器在运行,播放着你最喜欢的歌曲。下面就是两个进程并行工作的例子:一个运行电子表格程序,一个运行媒体播放器。众所周知,"多任务 "就是这个意思。仔细观察媒体播放器就会发现,在一个进程中又有多件事情在并行处理。在媒体播放器向音频驱动程序发送音乐的同时,用户界面也在不断更新。这就是线程的作用--单个进程内的并发性。

那么并发是如何实现的呢?单核 CPU 上的并行工作是一种错觉,有点类似于电影中移动图像的错觉。对于进程来说,这种错觉是通过在很短的时间内中断处理器在一个进程上的工作而产生的。然后,处理器继续处理下一个进程。为了在进程间切换,需要保存当前的程序计数器,并加载下一个处理器的程序计数器。这还不够,因为还需要对寄存器以及某些架构和操作系统特定的数据进行同样的处理。

就像一个 CPU 可以驱动两个或多个进程一样,CPU 也可以在一个进程的两个不同代码段上运行。当一个进程启动时,它总是执行一个代码段,因此该进程被称为一个线程。但是,程序可能会决定启动第二个线程。这样,两个不同的代码序列就会在一个进程内同时处理。在单核 CPU 上,并发是通过重复保存程序计数器和寄存器,然后加载下一个线程的程序计数器和寄存器来实现的。活动线程之间的循环无需程序的配合。当切换到下一个线程时,线程可能处于任何状态。

当前 CPU 设计的趋势是拥有多个内核。典型的单线程应用程序只能使用一个内核。但是,多线程程序可以分配给多个内核,从而实现真正的并发运行。因此,将工作分配给多个线程可以使程序在多核 CPU 上运行得更快,因为可以使用更多的内核。

图形用户界面线程和工作线程

如前所述,每个程序启动时都有一个线程。这个线程称为 "主线程"(在 Qt GUI 应用程序中也称为 "GUI 线程")。Qt GUI 必须在此线程中运行。所有部件和几个相关类(例如QPixmap )都不能在辅助线程中运行。二级线程通常被称为 "工作线程",因为它用于卸载主线程的处理工作。

同时访问数据

每个线程都有自己的堆栈,这意味着每个线程都有自己的调用历史和局部变量。与进程不同,线程共享同一个地址空间。下图显示了线程的构件在内存中的位置。非活动线程的程序计数器和寄存器通常保存在内核空间。每个线程都有一份共享代码副本和一个单独的堆栈。

"线程可视化"

如果两个线程拥有指向同一对象的指针,那么两个线程就有可能同时访问该对象,这就有可能破坏对象的完整性。可以想象,如果同时执行同一对象的两个方法,可能会出现很多问题。

有时需要从不同的线程访问一个对象,例如,生活在不同线程中的对象需要进行通信。由于线程使用相同的地址空间,因此线程交换数据比进程交换数据更方便、更快捷。数据无需序列化和复制。可以传递指针,但必须严格协调哪个线程接触哪个对象。必须防止在一个对象上同时执行操作。有几种方法可以做到这一点,下面将介绍其中的一些。

那么,怎样才能做到安全呢?在一个线程中创建的所有对象都可以在该线程中安全使用,前提是其他线程没有对这些对象的引用,而且对象与其他线程之间没有隐式耦合。这种隐式耦合可能发生在实例之间共享数据时,如静态成员、单子或全局数据。熟悉线程安全和可重入类与函数的概念。

使用线程

线程基本上有两种使用情况:

  • 利用多核处理器加快处理速度。
  • 通过将长期处理或阻塞调用卸载到其他线程,保持图形用户界面线程或其他时间关键线程的响应速度。

何时使用线程替代品

开发人员需要非常小心地使用线程。启动其他线程很容易,但要确保所有共享数据保持一致却非常困难。问题通常很难发现,因为它们可能只是偶尔出现一次,或者只出现在特定的硬件配置上。在创建线程来解决某些问题之前,应考虑可能的替代方案。

替代方案评论
QEventLoop::processEvents()在耗时的计算过程中反复调用QEventLoop::processEvents() 可以防止 GUI 阻塞。但是,这种解决方案并不能很好地扩展,因为对 processEvents() 的调用可能过于频繁,也可能不够频繁,这取决于硬件。
QTimer有时,后台处理可以使用定时器在未来某个时间安排槽的执行,非常方便。如果定时器的时间间隔为 0,一旦没有更多事件需要处理,定时器就会超时。
QSocketNotifier QNetworkAccessManager QIODevice::readyRead()这是在缓慢的网络连接上使用一个或多个线程(每个线程都有一个阻塞读取)的替代方法。只要响应网络数据块的计算能快速执行,这种反应式设计就比线程同步等待好。与线程相比,反应式设计更不容易出错,也更节能。在许多情况下,还能带来性能上的优势。

一般来说,建议只使用安全且经过测试的路径,避免引入临时线程概念。QtConcurrent 模块为将工作分配到处理器的所有内核提供了一个简单的接口。线程代码完全隐藏在QtConcurrent 框架中,因此您无需处理细节问题。不过,当需要与运行中的线程进行通信时,QtConcurrent 不能使用,也不能用于处理阻塞操作。

您应该使用哪种 Qt 线程技术?

请参阅 "Qt 中的多线程技术"页面,了解 Qt 多线程的不同方法,以及如何从中选择的指南。

Qt 线程基础知识

以下章节介绍了 QObjects 如何与线程交互、程序如何从多个线程安全地访问数据,以及异步执行如何在不阻塞线程的情况下产生结果。

QObject 与线程

如上所述,开发人员在从其他线程调用对象的方法时必须始终小心谨慎。Thread affinity 不会改变这种情况。Qt 文档将几个方法标记为线程安全。postEvent() 就是一个值得注意的例子。线程安全方法可同时从不同线程调用。

在通常不存在并发访问方法的情况下,调用其他线程中对象的非线程安全方法可能会在并发访问发生前工作数千次,从而导致意想不到的行为。编写测试代码并不能完全确保线程的正确性,但它仍然很重要。在 Linux 上,Valgrind 和 Helgrind 可以帮助检测线程错误。

保护数据完整性

编写多线程应用程序时,必须格外注意避免数据损坏。有关如何安全使用线程的讨论,请参阅同步线程。

处理异步执行

获取工作线程结果的一种方法是等待线程终止。但在很多情况下,阻塞等待是不可接受的。阻塞等待的替代方法是通过发布事件或队列信号和插槽进行异步结果交付。这会产生一定的开销,因为操作的结果不会出现在下一行源代码中,而是出现在源文件中其他地方的槽中。Qt GUI 开发人员习惯于使用这种异步行为,因为它与 GUI 应用程序中使用的事件驱动编程非常相似。

示例

Qt 提供了多个使用线程的示例。简单示例请参见QThreadQThreadPool 的类引用。有关更高级的示例,请参阅线程和并行编程示例页面。

深入探讨

线程是一个非常复杂的主题。Qt 提供的线程类比本教程介绍的要多。以下资料可帮助您更深入地了解这一主题:

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.