线程基础

什么是线程?

线程是并行地做事情,仅仅像进程。那么,线程与进程有何不同?若在电子表格中做计算,可能还有媒体播放器在同一桌面中运行,播放您最喜爱的歌曲。这是 2 个进程的并行工作范例:一个运行电子表格程序;另一个运行媒体播放器。为此,多任务处理是众所周知的术语。仔细观察媒体播放器会发现,在一个单进程中还有事情在并行。当媒体播放器将音乐发送给音频驱动器时,具有所有花哨功能的用户界面还在不断更新。这就是线程 -- 在一个单进程内并发。

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

就像一个 CPU 可以驱动 2 个或多个进程,也可以让 CPU 运行在一个单进程的 2 个不同代码段中。当进程启动时,它始终执行一代码段,因此说进程拥有一线程。不管怎样,程序可能决定启动第 2 线程。然后,在一个进程内同时处理 2 个不同的代码序列。通过重复保存程序计数器和寄存器,然后加载下一线程的程序计数器和寄存器,在单核 CPU 中达成并发。在活动线程之间循环,不需要程序的合作。当切换到下一线程出现时,线程可能处于任何状态。

CPU 设计的当前趋势是拥有多个核心。典型的单线程应用程序只能使用一个核心。不管怎样,可以将具有多个线程的程序赋值给多个核心,从而使事情以真正的并发方式发生。结果,将工作分发给多个线程可以使程序在多核 CPU 上运行得更快,因为可以使用其它核心。

GUI 线程和工作线程

如前所述,每个程序拥有一线程当它启动时。该线程被称为主线程 (在 Qt 应用程序中又称为 GUI 线程)。Qt GUI 必须在此线程中运行。所有 Widget 和几个相关类,例如 QPixmap ,不工作于第 2 线程。第 2 线程通常称为工作者线程,因为它用于从主线程分担处理工作。

同时访问数据

每个线程拥有自己的堆栈,意味着每个线程拥有自己的调用历史和局部变量。不像进程,线程共享相同地址空间。以下简图展示如何在内存中定位线程构造块。非活动线程的程序计数器和寄存器通常保持在内核空间中。有共享代码副本,且每个线程有单独堆栈。

Thread visualization

若 2 线程拥有相同对象指针,则 2 线程同时访问该对象是可能的,且这可能潜在破坏对象的完整性。很容易想象很多事情可能出错,当同一对象的 2 方法同时执行时。

有时有必要从不同线程访问某一对象;例如,当活在不同线程中的对象需要通信时。由于线程使用相同地址空间,线程交换数据更容易且更快,相比进程。不必序列化和拷贝数据。传递指针是可能的,但必须严格协调什么线程接触哪个对象。必须防止在一对象上同时执行操作。有几种办法能达成这且下文将描述其中一些办法。

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

使用线程

基本上,线程有 2 种使用案例:

  • 利用多核处理器提高处理速度。
  • 保持 GUI 线程或其它时间临界线程响应,通过分担长时间处理或阻塞其它线程的调用。

何时使用线程替代

开发者采用线程时需要很小心。启动其它线程很容易,但很难确保所有共享数据仍然一致。问题经常难以发现,因为它们可能仅偶尔出现一次,或仅在特定硬件配置上出现。在创建线程解决某些问题前,应考虑可能的替代。

Alternative 注释
QEventLoop::processEvents () 调用 QEventLoop::processEvents () 重复在耗时计算期间防止 GUI 阻塞。然而,此解决方案伸缩性不好,因为调用 processEvents() 发生次数可能过多或不足,从属硬件。
QTimer 有时可以使用计时器方便履行后台处理,以在将来某个时间点调度槽的执行。0 间隔计时器将尽快超时,一旦没有更多要处理的事件。
QSocketNotifier QNetworkAccessManager QIODevice::readyRead () 这是拥有一个或多个线程的替代,每个在缓慢网络连接上阻塞读取。只要可以快速执行响应网络数据组块的计算,这种反应式设计就比等待线程同步更优。反应式设计比线程更不易于出错且高效节能。在许多情况下,还有性能好处。

一般而言,只推荐使用安全且经过测试的路线,避免引入特别线程概念。 QtConcurrent 模块提供简易接口以将工作分发到所有处理器核心。 线程代码完全隐藏在 QtConcurrent 框架,因此,不必关心细节。不管怎样, QtConcurrent 不可以使用当需要与正运行线程通信时,且不应将其用于处理阻塞操作。

应使用哪种 Qt 线程技术?

Qt 中的多线程技术 页面了解 Qt 多线程的不同介绍方式,及如何选择它们的有关指导方针。

Qt 线程基础

以下章节描述 QObject 如何与线程交互,程序如何安全地从多个线程访问数据,及异步执行如何不阻塞线程产生结果。

QObject 和线程

如上所述,开发者必须始终小心当从其它线程调用对象方法时。 线程亲缘关系 不改变此状况。Qt 文档编制将几种方法标记为线程安全。 postEvent() 是显著范例。可以从不同线程同时调用线程安全方法。

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

保护数据完整性

在编写多线程应用程序时,必须小心避免数据破坏。见 同步线程 了解如何安全使用线程的有关讨论。

处理异步执行

获得工作者线程结果的一种办法是等待线程终止。然而,在很多情况下,阻塞等待不可接受。阻塞等待的替代是采用发布事件、队列信号及槽异步交付结果。这会产生某些开销,因为操作结果不会出现在下一源代码行中,而是出现在定位源代码文件中某些位置的槽中。Qt 开发者习惯使用这种异步行为,因为它非常类似用于 GUI 应用程序的事件驱动编程。

范例

Qt 带有使用线程的一些范例。见类参考对于 QThread and QThreadPool 了解简单范例。见 线程和并发编程范例 页面了解更高级的。

深入挖掘

线程是很复杂的主题。Qt 提供更多线程类,相比在此教程中呈现的。以下材料可帮您更深入研究该主题: