Qt之线程基础

何为线程

线程与并行处理任务息息相关,就像进程一样。那么,线程与进程有什么区别呢?当你在电子表格上进行数据计算的时候,在相同的桌面上可能有一个播放器正在播放你最喜欢的歌曲。这是一个两个进程并行工作的例子:一个进程运行电子表格程序;另一个进程运行一个媒体播放器。这种情况最适合用多任务这个词来描述。进一步观察媒体播放器,你会发现在这个进程内,又存在并行的工作。当媒体播放器向音频驱动发送音乐数据的时候,用户界面上与之相关的信息不断地进行更新。这就是单个进程内的并行线程。

那么,并发是如何实现的呢?在单核CPU计算机上,并行工作类似在电影院中不停移动图像产生的一种假象。对于进程而言,在很短的时间内中断占有处理器的进程就形成了这种假象。然而,处理器迁移到下一个进程。为了在不同进程之间进行切换,当前程序计数器被保存,下一个程序计数器被加载进来。这还不够,相关寄存器以及一些体系结构和操作系统特定的数据也要进行保存和重新加载。

正如一个CPU可以驱动两个或多个进程一样,同样也可以让CPU在单个进程内运行不同的代码片段。当一个进程启动时,它总是执行一个代码片断从而该进程就被认为是拥有了一个线程。然而,该程序可以决定启动第二个线程。这样,在一个进程内部,两个不同的代码序列就需要被同步处理。通过不停地保存当前线程的程序计数器和相关寄存器,同时加载下一个线程的程序计数器和相关寄存器,就可以在单核CPU上实现并行。在不同活跃线程之间的切换不需要这些线程之间的任何协作。当切换到下一个线程时,当前线程可能处于任一种状态。

当前CPU设计的趋势是拥有多核。一个典型的单线程应用程序只能利用一个核。但是,一个多线程程序可被分配给多个核,使得程序以一种完全并行的方式运行。这样,将一个任务分配给多个线程使得程序在多核CPU计算机上的运行速度比传统的单核CPU计算机上的运行速度快很多。

  • 何为线程
  • GUI线程和工作者线程
  • 数据的同步访问
  • 使用线程
    • 何时不应使用线程
    • 该使用哪种Qt线程技术
  • Qt 线程基础
    • QObject和Threads
    • 保护数据的完整性
    • 处理异步执行
  • 示例
  • 深入研究

GUI线程和工作者线程

如上所述,每个程序启动后就会拥有一个线程。该线程称为“主线程”(在Qt应用程序中也叫“GUI线程”)。Qt GUI必须运行在此线程上。所有的部件和几个相关的类,例如:QPixmap,不能工作于次线程中。次线程通常称为“工作者线程”,因为它主要处理从主线程中卸下的一些工作。

数据的同步访问

每个线程都有自己的栈,这意味着每个线程都拥有自己的调用历史和本地变量。不同于进程,线程共享相同的地址空间。下图显示了内存中的线程块图。非活跃线程的程序计数器和相关寄存器通常保存在内核空间中。对每个线程来说,存在一个共享的代码片段和一个单独的栈。

如果两个线程拥有一个指向相同对象的指针,那么两个线程可以同时去访问该对象,这可能潜在地破坏对象的完整性。很容易想象,一个对象的两个方法同时执行可能会出错。

有时,从不同线程中访问一个对象是不可避免的。例如:当位于不同线程中的许多对象之间需要进行通信时。由于线程之间使用相同的地址空间,线程之间进行数据交换要比进程之间进行数据交换快得多。数据不需要序列化然后拷贝。线程之间允许传递指针,但是必须严格协调哪些线程使用哪些指针。禁止在同一对象上执行同步操作。有一些方法可以实现这种要求,下面描述其中的一些方法。

那么,怎样做才安全呢?在一个线程中创建的所有对象在线程内部使用是安全的,前提条件是其它线程没有引用该线程中创建的一些对象且这些对象与其它的线程之间没有隐性耦合关系。当数据作为静态成员变量,单例或全局数据方式共享时,这种隐性耦合是可能发生的。

使用线程

对线程来讲,基本上有两种使用情形:

  • 利用多核处理器使处理速度更快。
  • 将一些处理时间较长或阻塞的任务移交给其它的线程,从而保证GUI线程或其它对时间敏感的线程保持良好的反应速度。

何时不应使用线程

开发者在使用线程时必须特意小心。启动其它线程很容易,但很难保证所有共享的数据保持一致。问题通常很难找到,因为它们可能在某个时候仅显示一次或仅在某种硬件配置下出现。在创建线程解决某些问题之前,应予以考虑可能的替代方案。

非线程方式 描述
QEventLoop::processEvents() 在一个耗时的计算中反复调用QEventLoop::processEvents()以免GUI被阻塞。但是,这种解决方式不能很好地扩展,因为会调用processEvents()可能太频繁或不够,取决于硬件。
QTimer 有时,在后台进程中使用一个计时器来调度在将来某个时间点运行一段程序非常方便。超时时间为0的计时器将在事件处理完后立即触发。
QSocketNotifier
QNetworkAccessManager
QIODevice::readyRead()
当在一个低速的网络连接上进行阻塞读的时候,可以不使用多线程。只要对一块网络数据的计算可以很快地执行,那么,这种交互式的设计比线程中的同步等待要好些。交互式设计比多线程要不容易出错且更有效。在许多情况下,也有一些性能上的提升。

一般情况下,建议只使用安全的且已被验证过的路径,避免引入线程概念。QtConcurrent提供了一种简易的接口,来将工作分配到所有的处理器的核上。线程相关代码已经完全隐藏在QtConcurrent 框架中,因此,开发者不需要关注这些细节。但是, QtConcurrent 不能用于那么需要与运行中的线程进行通信的情形,且它也不能用于处理阻塞操作。

该使用哪种Qt线程技术?

参见:Multithreading Technologies in Qt,介绍在不同途径中使用Qt多线程,以及如何选择其中的准则。

Qt 线程基础

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

QObject和Threads

如上所述,当从其它线程调用objects的函数时,开发人员必须经常关心。Thread affinity没有改善这种状况,Qt文档中将一些方法标记为线程安全, postEvent()就是一个值得注意的例子。一个线程安全的方法可以同时在不同的线程被调用。

通常情况下,并不会并发访问一些方法,在其它线程中调用对象的非线程安全方法,在并发访问前可能运行了数千次,出现意想不到的行为。编写测试代码不能完全确保线程的正确性,但它仍然是重要的。在Linux上,Valgrind和Helgrind有助于检测线程错误。

保护数据的完整性

在编写多线程应用程序时,需要格外小心,一定要注意避免数据损坏。见:Synchronizing Threads,了解如何安全地使用线程。

处理异步执行

一种获得工作者线程结果的方式是等待该线程停止。然而,在许多情况下,阻塞的等待是不可接受的。另一种方式是通过发送的事件或queued信号和槽来获得异步结果。这产生了一些开销,因为一个操作的结果并不是出现在下一个代码行,而是在一个位于其它地方的槽中。Qt开发者习惯了这种异步行为,因为它与GUI应用程序中事件驱动的方式非常类似。

示例

Qt提供了使用线程的一些例子,参考:QThread和QThreadPool简单的示例,更高级的请参考:Threading and Concurrent Programming Examples。

深入研究

线程是一个非常复杂的课题。Qt提供了更多比我们介绍过的线程相关类,下面内容可以帮助你更深入的进入主题:

  • Qt中Thread Support in Qt是一个很好的参考文档起点。
  • Qt提供了一些额外的例子QThread and QtConcurrent。
  • 一些好书介绍如何使用Qt线程。覆盖面最广的是《Qt高级编程》(作者:Mark Summerfield),共500页左右,大约有70页覆盖了QThread和QtConcurrent。
时间: 2024-07-29 04:39:26

Qt之线程基础的相关文章

Android多线程研究(1) 线程基础及源码剖析

从今天起我们来看一下Android中的多线程的知识,Android入门容易,但是要完成一个完善的产品却不容易,让我们从线程开始一步步深入Android内部. 一.线程基础回顾 package com.maso.test; public class TraditionalThread { public static void main(String[] args) { /* * 线程的第一种创建方式 */ Thread thread1 = new Thread(){ @Override publi

线程基础(第二部分)Java线程的缺陷和副作用几解决办法

<--在线程基础的第二部分中,我们将了解一下使用Java线程的缺陷和副作用,以及在SUN JDK 1.2中是如何修改线程的运行机制的--> 在上篇文章<Java 101之线程基础>中,我们介绍了线程的概念以及如何使用线程.这个月,我们将转到更高级的话题,包括线程的缺陷及副作用,以及在SUN JDK 1.2中,是如何改进线程的运行机制的. synchronize(同步) 让我们回忆一下上篇文章中讲的:线程允许两个或者更多个进程同时执行.实际上,这些线程也可以共享对象和数据,在这种情形

java线程编程(一):线程基础

在学习java中,我发现有关于对线程的讲解比较少,我打算为一些java初学者提一些关于线程方面的参考, 为深入学习java奠定基础.我本着共同进步的原则特写下了关于java线程编程的一系列文章 java线程编程(一):线程基础 ◆线程(thread)其实是控制线程(thread of control)的缩写. 每一个线程都是独立的,因此线程中的每个方法的局部变量都是和其他线程隔离开的,这些变量完全是私有的,因此对于 线程而言,是没有办法访问其他线程的局部变量的.如果两个线程同时访问同一个方法,则

Linux 系统应用编程——线程基础

 传统多任务操作系统中一个可以独立调度的任务(或称之为顺序执行流)是一个进程.每个程序加载到内存后只可以唯一地对应创建一个顺序执行流,即传统意义的进程.每个进程的全部系统资源是私有的,如虚拟地址空间,文件描述符和信号处理等等.使用多进程实现多任务应用时存在如下问题: 1)任务切换,即进程间上下文切换,系统开销比较大.(虚拟地址空间以及task_struct 都需要切换) 2)多任务之间的协作比较麻烦,涉及进程间通讯.(因为不同的进程工作在不同的地址空间) 所以,为了提高系统的性能,许多操作系统规

[CLR via C#]25. 线程基础

原文:[CLR via C#]25. 线程基础 一.Windows为什么要支持线程 Microsoft设计OS内核时,他们决定在一个进程(process)中运行应用程序的每个实例.进程不过是应用程序的一个实例要使用的资源的一个集合.每个进程都赋予了一个虚拟地址空间,确保一个进程使用的代码和数据无法由另一个进行访问.这样就确保了应用程序集的健壮性,因为一个进程无法破坏另一个进程里的数据和代码.另外,进程是无法访问到OS的内核代码和数据. 如果一个应用程序进入死循环时,如果只是单核的CPU的,它会无

Qt之线程同步(生产者消费者模式 - QWaitCondition)

简述 生产者将数据写入缓冲区,直到它到达缓冲区的末尾,这时,它从开始位置重新启动,覆盖现有数据.消费者线程读取数据并将其写入标准错误. Wait condition(等待条件)比单独使用 mutex(互斥量)有一个更高级的并发性,如果缓冲区的访问由一个 QMutex 把守,当生产者线程访问缓冲区时,消费者线程将无法访问.然而,两个线程同时访问不同的缓冲区是没有害处的. 示例包含两个类:Producer 和 Consumer,均继承自 QThread.循环缓冲区用于两个类之间的沟通,同步工具用于保

Qt之线程同步

简述 使用线程的目的是允许代码并行运行,但是有时线程必须停止并等待其他线程.例如,如果两个线程试图同时写入相同的变量,结果是未知的. 迫使线程等待另一个的原则被称为互斥 . 这是一种保护共享资源等数据的常见的技术. 简述 低级同步原语 风险 便利类 高级事件队列 低级同步原语 QMutex 是强制执行互斥的基本类.一个线程锁定一个互斥量(mutex),以获得共享资源的访问权限.如果另一个线程试图锁定互斥量,而互斥量已被锁定,这时,它将进入睡眠状态,直到第一个线程完成其任务并解锁互斥量. QRea

Qt之线程同步(生产者消费者模式 - QSemaphore)

简述 生产者将数据写入缓冲区,直到它到达缓冲区的末尾,此时,它将从开始位置重新启动,覆盖现有数据.消费者线程读取数据并将其写入标准错误. Semaphore(信号量) 比 mutex(互斥量)有一个更高级的并发性.如果缓冲区的访问由一个 QMutex 把守,当生产者线程访问缓冲区时,消费者线程将无法访问.然而,有两个线程同一时间访问不同的缓冲区是没有害处的. 示例包括两个类:Producer 和 Consumer,均继承自 QThread.循环缓冲区用于这两个类之间的沟通,信号量用于保护全局变量

线程基础之遗漏和扩展部分

  这里我们只是关注了一些多线程之间共享变量的简单使用问题.这些是任何一个写多线程程序的人,都应该熟悉的最基础的问题.我们忽略了一些其他多线程实现提供的工具.它们虽然很少被用到,但是对于你的程序仍然很有必要. 其他锁类型 大多数环境提供可重入锁,即被一个单线程多次持有,比如java synchronized 块就有这种锁的特性行为.通常读写锁也提供这个功能,即一个锁可以同时被多个"读"线程持有,但只能同时被一个"写"线程持有. 条件变量等 对于一个线程等待某个特殊条