Qt线程基础
- 一、什么是线程?
- 二、GUI线程和工作线程
- 三、同时访问数据
- 四、使用线程
- 1、何时使用线程的替代品
- 2、应该用哪种Qt线程技术?
- 六、Qt中的多线程技术
- 1、QThread:带有可选事件循环的低级API
- 2、QThreadPool和QRunnable:重用线程
- 七、Qt Concurrent:使用高级API
- 八、WorkerScript:QML的线程
- 九、选择合适的方法
- 1、示例使用案例
一、什么是线程?
线程就是并行地做事情,就像进程一样。那么线程和进程有什么不同呢?当您在电子表格上进行计算时,可能会有一个运行在同一桌面上的媒体播放器播放您最喜欢的歌曲。
这是两个进程并行工作的例子:
一个运行电子表格程序;
一个运行媒体播放器。
对此,多任务处理是一个众所周知的术语。仔细观察媒体播放器会发现,在一个进程中又有一些事情在并行进行。当媒体播放器向音频驱动程序发送音乐时,用户界面及其各种功能也在不断更新。这就是线程的用途——在一个进程中实现并发。
那么并发是如何实现的呢?单核CPU上的并行工作是一种错觉,有点类似于电影中的运动图像错觉。对于进程来说,这种错觉是通过在很短的时间后中断处理器在一个进程上的工作而产生的。然后处理器进入下一个进程。为了在进程之间切换,保存当前程序计数器,并加载下一个处理器的程序计数器。这是不够的,因为需要对寄存器和某些架构以及操作系统特定的数据进行同样的处理。
正如一个CPU可以驱动两个或多个进程一样,也可以让CPU在一个进程的两个不同代码段上运行。
当一个进程启动时,它总是执行一个代码段,因此这个进程被称为有一个线程。但是,程序可能会决定启动第二个线程。然后,在一个进程中同时处理两个不同的代码序列。通过重复保存程序计数器和寄存器,然后加载下一个线程的程序计数器和寄存器,可以在单核CPU上实现并发性。在活动线程之间循环不需要来自程序的合作。当切换到下一个线程时,线程可以处于任何状态。
CPU设计的当前趋势是拥有几个核心。典型的单线程应用程序只能使用一个内核。然而,具有多个线程的程序可以被分配到多个内核,使得事情以真正并发的方式发生。因此,将工作分配给多个线程可以使程序在多核CPU上运行得更快,因为可以使用额外的内核。
二、GUI线程和工作线程
如上所述,每个程序在启动时都有一个线程。这个线程被称为“主线程”(在Qt应用中也称为“GUI线程”)。
Qt GUI必须在这个线程中运行。所有小部件和几个相关的类,例如QPixmap
,不要在副线程中工作。辅助线程通常被称为“工作线程”,因为它用于从主线程卸载处理工作。
三、同时访问数据
每个线程都有自己的堆栈,这意味着每个线程都有自己的调用历史和局部变量。与进程不同,线程共享相同的地址空间。
下图显示了线程的构造块在内存中的位置。非活动线程程序计数器和寄存器通常保存在内核空间中。每个线程都有一个共享的代码副本和一个单独的堆栈。
#子线程
worker thread : 工作线程; 辅助线程; 背景工作线程;
program counter : 程序计数器;
register : 寄存器;
#主线程 程序入口
main thread : 主线程;
program counter : 程序计数器;
register : 寄存器;
#CPU 芯片
program counter : 程序计数器;
register : 寄存器;
#CPU <-> 子线程
cyclical load / unload : 循环加载/卸载
memory : 内存;
kernel space : 内核空间;
________________________________________
user space : 用户空间;
code : 代码段;
stack main : 主栈 (主线程栈区)
stack worker : 工作栈(子线程栈区)
heap : 堆 (一般又程序员主动申请)
process : 进程;
_______________________________________
other process : 其他进程;
如果两个线程有一个指向同一个对象的指针,那么两个线程可能会同时访问该对象,这可能会破坏对象的完整性。
当同一个对象的两个方法同时执行时,很容易想象会有多少事情出错。
有时需要从不同的线程访问一个对象;
例如,当生活在不同线程中的对象需要通信时。因为线程使用相同的地址空间,所以线程交换数据比进程更容易、更快。数据不必序列化和复制。传递指针是可能的,但是必须严格协调哪个线程接触哪个对象。必须防止在一个对象上同时执行操作。
有几种方法可以实现这一点,下面介绍其中一些方法。
那么,什么是安全的呢?在一个线程中创建的所有对象都可以在该线程中安全地使用,前提是其他线程没有对它们的引用,并且对象没有与其他线程的隐式耦合。这种隐式耦合可能发生在实例之间共享数据时,如静态成员、单例或全局数据。熟悉…的概念线程安全和可重入类和函数。
四、使用线程
线程基本上有两种用例:
- 利用多核处理器加快处理速度。
- 通过卸载持续时间长的处理或阻塞对其他线程的调用,保持GUI线程或其他时间关键型线程的响应。
1、何时使用线程的替代品
开发人员需要非常小心线程。启动其他线程很容易,但很难确保所有共享数据保持一致。问题通常很难发现,因为它们可能只是偶尔出现一次,或者只出现在特定的硬件配置上。在创建线程来解决某些问题之前,应该考虑可能的替代方案。
供选择的 | 评论 |
---|---|
QEventLoop::processEvents() | QEventLoop::processEvents 在耗时的计算过程中重复()可防止GUI阻塞。然而,这种解决方案不能很好地扩展,因为根据硬件的不同,对processEvents()的调用可能太频繁,或者不够频繁。 |
QTimer | 后台处理有时可以方便地使用定时器来调度在未来某个时间点的槽的执行。一旦没有更多的事件要处理,间隔为0的计时器就会超时。 |
QSocketNotifier QNetworkAccessManager qiode device::ready read() | 这是拥有一个或多个线程的替代方案,每个线程在慢速网络连接上有一个阻塞读取。只要响应大量网络数据的计算能够快速执行,这种反应式设计就比线程中的同步等待要好。反应式设计比线程设计更不易出错且更节能。在许多情况下,还有性能优势。 |
一般来说,建议只使用安全且经过测试的路径,并避免引入临时线程概念。这Qt并发模块提供了一个简单的接口,用于将工作分配给处理器的所有内核。线程代码完全隐藏在Qt并发框架,这样就不用管细节了。然而,Qt并发当需要与正在运行的线程进行通信时,不能使用它,并且它不应该用于处理阻塞操作。
2、应该用哪种Qt线程技术?
参见Qt中的多线程技术页面,了解Qt多线程处理的不同方法,以及如何选择这些方法的指南。
上面是官方英文链接,这里我就翻译过来讲
六、Qt中的多线程技术
Qt提供了许多处理线程的类和函数。下面是Qt程序员可以用来实现多线程应用的四种不同方法。
1、QThread:带有可选事件循环的低级API
QThread是Qt中所有线程控制的基础。每个QThread实例表示并控制一个线程。
QThread可以直接实例化,也可以子类化。
实例化QThread提供并行事件循环,允许QObject要在辅助线程中调用的插槽。
子类化QThread允许应用程序在开始事件循环之前初始化新线程,或者在没有事件循环的情况下运行并行代码。
2、QThreadPool和QRunnable:重用线程
频繁地创建和销毁线程可能代价很高。为了减少这种开销,现有线程可以重新用于新任务。QThreadPool是可重用QThreads的集合。
在其中一个QThreadPool’s线程,重新实现
QRunnable::run()
并实例化子类QRunnable
。使用QThreadPool::start()
把QRunnable
在QThreadPool’s运行队列。当线程变得可用时,其中的代码QRunnable::run()
将在该线程中执行。
每个Qt应用程序都有一个全局线程池,可以通过
QThreadPool::globalInstance().
这个全局线程池根据CPU中核心的数量自动维护最佳的线程数量。然而,一个单独的QThreadPool可以显式地创建和管理。
七、Qt Concurrent:使用高级API
这
Qt Concurrent
模块提供了处理一些常见并行计算模式的高级函数:映射、过滤和归约
。
不同于使用QThread
和QRunnable
,这些函数从不需要使用低级线程原语例如互斥或信号量。相反,它们返回一个QFuture
对象,该对象可用于在函数准备就绪时检索函数的结果。QFuture
也可用于查询计算进度和暂停/恢复/取消计算。为了方便起见,QFutureWatcher
支持与以下对象的交互QFuture
通过信号和插槽。
Qt Concurrent
’s的map、filter和reduce算法会自动在所有可用的处理器内核之间分配计算,因此,今天编写的应用程序将在以后部署到具有更多内核的系统上时继续扩展。本模块还提供了
QtConcurrent::run()
函数,可以运行另一个线程中的任何函数。然而,QtConcurrent::run()
仅支持可用于映射、过滤和归约功能的功能子集。
这QFuture
可用于检索函数的返回值并检查线程是否正在运行。然而,调用QtConcurrent::run()
仅使用一个线程,不能暂停/恢复/取消,也不能查询进度。
八、WorkerScript:QML的线程
这
WorkerScript
QML类型让JavaScript代码与GUI线程并行运行。每个
WorkerScript
实例可以有一个.js附在它上面的脚本。当WorkerScript.sendMessage()
被调用时,脚本将在一个单独的线程(和一个单独的QML背景).当脚本完成运行时,它可以向GUI线程发送一个回复,该线程将调用WorkerScript.onMessage()
信号处理器。
九、选择合适的方法
如上所述,Qt为开发线程化应用提供了不同的解决方案。给定应用程序的正确解决方案取决于新线程的用途和线程的生命周期。下面是Qt线程技术的比较,以及一些示例用例的推荐解决方案。
1、示例使用案例
线程的寿命 | 操作 | 解决办法 |
单次调用 | 在另一个线程中运行一个新的线性函数,可选地在运行期间进行进度更新。 | Qt提供不同的解决方案: 【1】将函数放在的重新实现中QThread::run() 并启动QThread 。发出信号以更新进度。【2】将函数放在的重新实现中QRunnable::run() 并添加QRunnable 到QThreadPool 。写信给线程安全变量更新进度。【3】使用运行该函数QtConcurrent::run() .写信给线程安全变量更新进度。 |
单次调用 | 在另一个线程中运行现有函数,并获取其返回值。 | 使用运行该函数QtConcurrent::run(). 有一个QFutureWatcher 发出finished() 当函数返回时发出信号,并调用QFutureWatcher::result() 获取函数的返回值。 |
单次调用 | 使用所有可用的核心对容器中的所有项目执行操作。例如,从图像列表中生成缩略图。 | 使用Qt并发QtConcurrent::filter() 函数来选择容器元素,而QtConcurrent::map() 函数将运算应用于每个元素。要将输出合并成一个结果,请使用QtConcurrent::filteredReduced() 和QtConcurrent::mappedReduced() 取而代之。 |
单次调用/永久调用 | 在纯QML应用程序中执行长时间的计算,并在结果准备好时更新GUI。 | 将计算代码放在.js脚本并将其附加到WorkerScript 实例。调用WorkerScript.sendMessage() 在新线程中开始计算。让脚本也调用sendMessage() ,将结果传递回GUI线程。处理结果onMessage 并在那里更新GUI。 |
永久调用 | 让一个对象驻留在另一个线程中,该线程可以根据请求执行不同的任务和/或可以接收新数据进行处理。 | 子类QObject创造一个任务。实例化该工作对象和一个QThread 。将工作线程移至新线程。通过排队的信号插槽连接向任务对象发送命令或数据。 |
永久调用 | 在另一个线程中重复执行开销较大的操作,该线程不需要接收任何信号或事件。 | 直接在的重新实现中编写无限循环QThread::run(). 在没有事件循环的情况下启动线程。让线程发出信号将数据发送回GUI线程。 |
后续安排例子