StupidBeauty
Read times:2061Posted at:Sat Oct 23 03:43:20 2021 - no title specified

Qt 5.15 文档翻译:多线程的基本概念,Threading Basics

什么是多线程?

多线程,其用途是并行地工作,就类似多进程那样。那么,多线程与多进程又有什么区别呢?当你在一个电子表格里做计算的同时,可能有一个多媒体播放器正在电脑上播放着你最爱听的歌。这就是两个进程并行工作的例子:一个进程运行着电子表格程序;另一个进程运行着多媒体播放器程序。关于这种并行工作,有一个知名的术语,叫做多任务处理。进一步深入观察多媒体播放器进程,你会发现,在单个进程内部,事物也是并行运行的。当多媒体播放器向音频驱动发送音乐内容时,用户界面上的各个通知性的声音也仍然会响起。这就是多线程起作用的地方——在单个进程内部实现并发。

那么,并发性,是如何实现的?在单核CPU上,并发工作只是一个假象,它类似于电影院里切换图片时产生的假象。对于多个进程来说,这个假象具体是这样的造成的:在很短的时间之后,让处理器中断它在某个进程上的工作。然后,处理器会处理下一个进程的工作。为了正确地在不同进程间切换,需要保存当前程序的执行计数器,并载入下一个进程的程序执行计数器。这还不够,还需要对以下事物做同样的事:寄存器;与架构和操作系统相关的特定数据。

单个CPU可以支持两个或更多的进程,同样地,CPU也可以在单个进程内部执行两段不同的代码段。当一个进程被启动之后,它保持着执行单个的代码段,这个时候,该进程只有一个线程。然而,该程序可能决定要启动第二个线程。之后,这单个进程里,就会有两个不同的代码序列被并行执行了。在单核CPU上,通过重复性的保存程序计数器和寄存器并载入下一个线程的程序计数器和寄存器的方式来实现并发。程序本身不需要做任何配合,即可在活跃的线程之间切换。在发生向下一个线程的切换时,当前线程可能处于任何状态。

在CPU设计中的当前趋势是,让它们拥有多个内核。一个典型的单线程程序,只能使用一个CPU内核。但是,具有多线程功能的程序,可以被放置到多个CPU内核上运行,使得各个事物真正地并行进行。由此产生的结果就是,创建多个线程,会使得程序在多核CPU上运行得更快,因为它可以利用额外的CPU内核。

图形界面线程和工作线程

前面已经说过,每个程序在启动时,都只有一个线程。这个线程被称为“主线程”(在Qt 程序中也称作“图形界面线程”)。Qt图形界面就必须在这个线程里运行。所有的部件类,以及一些相关的类(例如QPixmap),都无法在其它的附加线程中运行。附加线程,一般也被称作“工作线程”,因为,它被用来将处理工作从主线程移交出来。

并发访问数据

每个线程都有它自己的栈,这就意味着,每个线程有它自己的调用历史和本地变量。与多进程模式的不同之处在于,多个线程是共享相同的地址空间的。以下图表展示了多个线程相关的组件是如何在内存中分布的。非活跃的线程的程序计数器和寄存器一般是保存在内核空间中的。代码是共享的,而调用栈是各个线程独有一份的。

如果有两个线程都持有指向同一个对象的指针,那么,就可能出现一种情况,两个线程同时对该对象进行访问,这种情况有可能会破坏那个对象的完整性。不难想象,当同一个对象的两个不同方法被同时执行时,会出现很多问题。

有些时候确实有必要从不同的线程中访问同一个对象;例如,不同线程中的对象可能需要互相通信。由于多个线程之间使用的是相同的地址空间,所以,在线程间交换数据,比在进程间交换数据要容易得多,也快速得多。数据不需要序列化和复制。直接传递指针也是可行的,但是必须严格控制好,哪个线程能够访问哪个对象。必须禁止同时在同一个对象上执行多个操作。要实现这种控制,可采用多种方法,下面会说明其中的一些方法。

那么,哪些事情是可以安全地做的?在某个线程中创建的所有对象,都可以在该线程中安全地使用,只要其它线程不持有针对这些对象的引用,并且这些对象未与其它线程存在隐式的耦合,即可。这些情况下会产生隐式的耦合:通过静态成员变量、单实例或全局数据来共享数据。请先熟悉了解类和函数的线程安全和重入这个概念。

使用线程

线程的使用情况基本分成两种:

  • •.利用多核处理器加快处理速度。

  • •.将耗时较长的处理过程以及阻塞式的调用放置到其它线程中去执行,以确保图形界面线程及其它的对时间敏感的线程能够及时响应。

什么情况下应当采用多线程之外的解决方案

开发者在使用多线程时应当非常小心。要启动多线程是很容易的,但是,要做到确保所有共享的数据的一致性,是很难的。遇到问题时会很难发现其原因,因为,它们可能只是会偶然出现,或者只会在特定的硬件条件下出现。在采用多线程来解决特定问题之前,应当先考虑其它的替代方案。

替代方案

说明

QEventLoop::processEvents()

可在一个耗时较长的计算过程中多次重复调用QEventLoop::processEvents(),以避免图形界面卡住。但是,这个方案的兼容性不太好,因为,对processEvents()的调用很可能会过于频繁,或是不够频繁,具体是受硬件性能影响的。

QTimer

有些时候,可通过定时器来将一些后台处理任务推迟到未来的某个时刻来执行。如果将定时器的时间间隔设置为0,那么,当所有事件都处理完毕时,该定时器就会立即超时。

QSocketNotifier QNetworkAccessManager QIODevice::readyRead()

这种方案,是用来替代那种采用多个线程来从慢速网络连接中阻塞式读取数据的方案的。只要针对到来的网络数据的计算能够很快完成,这种响应式的设计就会比阻塞式地在线程中等待要好得多。响应式设计,相比于多线程式的设计,出错的概率更小,且效率更高。在很多情况下,还会得到性能提升。

一般来说,建议仅仅使用安全的、久经考验的方法来解决问题,而避免引入随意设计的多线程概念。QtConcurrent模块,提供了一个易用的接口,可用来将计算工作分发到处理器的所有内核。多线程的代码被完全隐藏到QtConcurrent框架的内部了,所以,你完全不用关心其中的细节。然而,当你需要与运行中的线程进行通信时,就不能使用QtConcurrent了,同时,它也不能用于处于阻塞式的操作。

你应当使用哪种Qt多线程技术?

参考Qt 中的多线程技术页面,以了解Qt 中可以采用的不同的多线程技术,以及该如何从中选择合适的技术。

Qt多线程基础知识

以下小节中,说明了,QObject是如何与线程进行交互的,程序中可以如何从多个线程中安全地访问数据,以及,异步的执行是如何在不阻塞线程的情况下产生结果的。

QObject与多线程

前面已经说过,当开发者从别的线程中调用对象的方法时,必须小心行事。即使用上了线程关联特性也不能改变这一点。Qt文档中已经将多个方法标记为线程安全的。postEvent()就是一个显著的例子。线程安全的方法,可被多个不同的线程同时调用。

如果你的代码中一般情况下很少有并发地对方法进行调用的情形,那么,调用其它线程中不具有线程安全性的方法,很可能会很多次都能正常工作,直到某一次真的发生了并发的访问为止,那时会产生出意料之外的行为。编写测试代码并不能完全确保线程相关的正确性,但是它仍然是一种很重要的手段。可使用Valgrind和Helgrind来帮助探测与多线程相关的错误。

保持数据的完整性

在编写多线程程序时,必须额外注意避免损坏数据。参考对多线程进行同步,以了解,该如何确保线程安全性。

处理异步执行相关的问题

在获取工作线程的结果时,可用的一种方法是,等待该线程结束。然而,在很多情况下,阻塞式的等待是不可接受的。作为阻塞式等待的替代方案,还可以采用异步的结果传递,具体手段可以是传送消息或是队列式的信号和信号槽。这会带来一定的额外开销,因为,一个操作的结果并不会立即出现在下一行代码中,而是出现在源代码中别的地方的某处信号槽中。Qt开发者们已经习惯了这种异步的行为,因为,它与图形用户界面程序中所采用的事件驱动编程是很类似的。

示例

Qt中自带了多个与多线程相关的示例。参考QThreadQThreadPool的类文档,以研究相关的简单示例。参考多线程及并发编程示例页面以研究更复杂的示例。

更深入研究

多线程是一个非常复杂的主题。Qt提供的用于多线程编程的类,比这个教程中展示的还要多。下述内容可帮助你更进一步深入了解这个主题:

青花袍

 吃烟

Your opinions
Your name:Email:Website url:Opinion content:
- no title specified

HxLauncher: Launch Android applications by voice commands