多线程基础

什么是线程?

线程是关于并行执行事务的,就像进程。那么线程与进程有何不同?当你在一个表格中进行计算时,同一台桌面上的媒体播放器可能也在播放你最喜欢的歌曲。这是一个两个进程并行工作的例子:一个运行表格程序;一个运行媒体播放器。多任务处理是一个众所周知的术语。仔细观察媒体播放器可以发现,一个进程内部也在并行进行一些操作。当媒体播放器向音频驱动程序发送音乐时,用户界面及其所有动态功能在持续更新。这就是线程的作用——一个进程内的并发。

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

就像一个CPU可以同时支持两个或多个进程一样,也可以让CPU在一个单一进程的两个不同代码段中运行。进程启动时,总是执行一个代码段,因此进程有一个线程。然而,程序可以决定启动第二个线程。然后,一个进程内的两个不同的代码序列可以同时处理。通过频繁地保存程序计数器和寄存器,再加载下一个线程的程序计数器和寄存器,在单核CPU上实现并发。程序不需要在活动线程之间循环时进行任何合作。线程可能在切换到下一个线程时处于任何状态。

当前CPU设计的趋势是具有多个核心。一个典型的单线程应用只能利用一个核心。然而,具有多个线程的程序可以分配到多个核心,从而使事情真正以并发的方式进行。因此,将工作分布到多个线程可以使程序在多核CPU上运行得更快,因为可以使用更多的核心。

GUI线程和工作线程

正如所提到的,程序启动时有一个线程。这个线程被称为“主线程”(在Qt应用程序中也称为“GUI线程”)。Qt GUI必须在主线程中运行。所有小部件以及一些相关类,例如 QPixmap,都不能在二级线程中工作。二级线程通常被称为“工作线程”,因为它用于从主线程卸载处理工作。

数据同时访问

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

"Thread visualization"

如果两个线程指向同一对象,那么这两个线程可能会同时访问该对象,这可能会破坏对象的完整性。想象一下,当同一对象的两个方法同时执行时,可能会发生许多错误。

有时需要从不同的线程访问同一对象;例如,当存在于不同线程中的对象需要通信时。由于线程使用相同的地址空间,线程之间交换数据比进程容易且快。数据不必序列化和复制。可以传递指针,但必须严格协调哪个线程访问哪个对象。必须防止对一个对象的操作同时执行。有几种实现方式,下面将描述其中的一些。

那么什么操作是安全的?在创建对象的线程中,只要其他线程没有对其的引用,并且对象没有与其他线程隐式耦合,就可以在その线程内部安全地使用所有这些对象。这种隐式耦合可能发生在数据在实例之间共享时,例如在静态成员、单例或全局数据中。熟悉线程安全和无重入类和函数的概念。

使用线程

线程基本有两种使用场景。

  • 利用多核处理器提高处理速度。
  • 通过将长持续处理或阻塞调用卸载到其他线程来保持GUI线程或其他时间关键线程的响应。

何时使用线程的替代方案

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

替代方案注释
QEventLoop::processEvents()在耗时的计算过程中重复调用QEventLoop::processEvents()可以防止GUI阻塞。然而,这种解决方案的扩展性不好,因为processEvents()的调用可能过于频繁或不够频繁,这取决于硬件。
QTimer有时可以使用计时器来方便地安排在未来的某个时刻执行槽函数的调用,从而在后台执行处理。具有0间隔的计时器将在没有更多事件处理时超时。
QSocketNotifier QNetworkAccessManager QIODevice::readyRead()这是在慢速网络连接上具有阻塞读操作的单个或多个线程的替代方案。只要对网络数据块的反应计算可以快速执行,这种反应式设计比线程中的同步等待更好。与线程相比,反应式设计错误 khởi phát较少,能源效率更高。在许多情况下,还存在性能优势。

一般情况下,建议只使用安全和经过测试的路径,以避免引入临时线程概念。QtConcurrent模块提供了一个方便的接口,用于将工作分配到处理器的所有核心。线程代码完全隐藏在QtConcurrent框架中,因此您无需关注细节。然而,在需要与运行线程通信时,无法使用QtConcurrent,并且不应该用它来处理阻塞操作。

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

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

Qt线程基础

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

QObject和线程

如上所述,开发人员必须始终谨慎地从其他线程调用对象的方法。《线程亲和力》不会改变这种情况。Qt文档标记了一些方法是线程安全的。postEvent() 是一个值得注意的例子。线程安全的方法可以从不同的线程同时调用。

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

保护数据完整性

在编写多线程应用程序时,必须特别注意避免数据损坏。请参阅《同步线程》讨论如何安全地使用线程。

处理异步执行

获取工作线程的结果的一种方式是通过等待线程终止。然而,在许多情况下,阻塞性的等待是不可接受的。阻塞等待的替代方案是使用非阻塞等待,无论是发布事件还是排队信号和槽。这会产生一定开销,因为操作的结果不会出现在下一源行,而是在源文件中的另一个槽中。Qt开发人员习惯了与这种类型的异步行为一起工作,因为它非常类似于在GUI应用程序中使用的驱动事件编程。

示例

Qt附带了一些线程使用示例。请参阅《QThread》和《QThreadPool》类的引用,以获取简单示例。请参阅《线程和并发编程示例》页面,以获取更高级的示例。

深入了解

线程是一个非常复杂的话题。Qt提供的线程类比我们在本教程中介绍的要多。以下材料可以帮助您深入了解这个主题

© 2024 Qt公司。此处包含的文档贡献归各自所有者所有。提供的文档根据自由软件基金会发布的GNU自由文档许可证第1.3版许可。Qt及其相应标志是芬兰及其它全球国家的商标,相关的商标属于各自所有者。所有其他商标均归各自所有者所有。