• 线程基础

    什么是线程?

    Threads are about doing things in parallel, just like processes. So how do threads differ from processes? While you are making calculations on a spreadsheet, there may also be a media player running on the same desktop playing your favorite song. Here is an example of two processes working in parallel: one running the spreadsheet program; one running a media player. Multitasking is a well known term for this. A closer look at the media player reveals that there are again things going on in parallel within one single process. While the media player is sending music to the audio driver, the user interface with all its bells and whistles is being constantly updated. This is what threads are for – concurrency within one single process.

    那么,如何实现并发呢?在单核 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 () is a noteworthy example. A thread-safe method may be called from different threads simultaneously.

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

    保护数据完整性

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

    处理异步执行

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

    范例

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

    深入挖掘

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