欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 旅游 > 【Linux系统】线程:线程的优点 / 缺点 / 超线程技术 / 异常 / 用途

【Linux系统】线程:线程的优点 / 缺点 / 超线程技术 / 异常 / 用途

2025/2/8 3:37:09 来源:https://blog.csdn.net/2301_79499548/article/details/145471075  浏览:    关键词:【Linux系统】线程:线程的优点 / 缺点 / 超线程技术 / 异常 / 用途




在这里插入图片描述



1、线程的优点


创建和删除线程代价较小


创建一个新线程的代价要比创建一个新进程小得多,删除代价也小。这种说法主要基于以下几个方面:

(1)资源共享

  • 内存空间:每个进程都有自己独立的内存空间,包括代码段、数据段和堆栈等。当创建一个新的进程时,操作系统需要分配新的内存空间,并复制父进程的内存内容(这被称为地址空间复制)。而线程是进程内的执行单元,所有同属一个进程的线程共享该进程的内存空间,因此创建线程时不需要进行地址空间的复制。

  • 系统资源:进程间不共享文件描述符、信号处理等系统资源,每个新进程都需要重新初始化这些资源。相比之下,同一进程内的多个线程可以共享文件描述符、信号处理器等资源。


(2)上下文切换

  • 创建新进程涉及到完整的上下文环境设置,包括用户态和内核态的各种数据结构的初始化。而创建线程时,由于大多数资源都是共享的,只需要建立线程控制块(TCB)以及分配栈空间等少量操作即可,因此上下文切换的成本更低。


(3)性能开销

  • 时间消耗:创建和销毁进程涉及大量的系统调用和内存操作,这通常需要更多的时间。线程由于减少了大量重复资源的分配与初始化工作,所以创建和销毁的速度更快,时间消耗更少。

  • 内存消耗:新进程需要分配单独的内存空间,导致内存占用相对较高;而新线程仅需分配栈空间(通常是几MB),因此内存消耗远小于创建新进程所需的消耗。




线程切换 vs 进程切换:线程切换代价较小


更加具体和详细的整理讲解可以看这篇博客:

【Linux系统】线程切换 vs 进程切换 :线程切换成本较低-CSDN博客


与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。

最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

(例如 CR3 寄存器存储页表的物理地址,线程切换不更换页表,则像 CR3 这类“共享的寄存器上下文就无需切换”)

另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲 TLB(快表)会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件 Cache。

意思是:多线程因为同属于同一个进程,线程之间可能会使用共享的数据或代码,因此 TLB 和 Cache 的缓存对其他线程也有帮助,而进程切换就一定要切换 TLB 和Cache 了,因为代码完全变了。



线程占用的资源要比进程少很多


同一进程内的所有线程共享该进程的大部分资源,包括内存地址空间、文件描述符、数据段等。

每个线程仅需要维护自己独立的线程控制块(TCB),这其中包括了线程的栈指针、程序计数器、寄存器集合等少量信息,因此线程占用的资源较少,切换速度也更快。

而进程之间的资源占用则多得多。每个进程都有自己的地址空间、文件描述符表、页表等,这些资源都是独立且私有的。这意味着进程之间的切换和通信开销更大,因为操作系统需要为每个进程分配独立的内存空间,并确保进程间的数据隔离与保护。



能充分利用多处理器的可并行数量。

每个处理器(这里考了单核处理器的情况)可调用一个线程,在多处理器系统下,本系统就可以同时调用多个线程,实现并行处理任务



其他优点


(1)在等待慢速 I/O 操作结束的同时,程序可执行其他的计算任务。

这个比较容易理解,例如有一个进程用于读取磁盘内容,磁盘 IO 属于慢速 IO,需要磁盘 IO 时,本进程可以创建一个线程专门用于磁盘 IO,可以创建其他线程用于其他工作任务,增加并行量,提升工作效率



(2)计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。

例如某计算密集型应用创建一个进程用于密集型计算,需要计算十万条数据,就可以创建10个线程分别计算一万条数据,在多处理器系统上,每个处理器一次调用一个线程,这样就能在多处理器系统上将计算分解到多个线程中



(3)I/O 密集型应用,为了提高性能,将 I/O 操作重叠。线程可以同时等待不同的 I/O 操作。

这种也一样,将 IO 任务分配给多个线程处理



问题:计算密集型应用,线程是否越多越好?

答:并不是,若线程过多,则问题就由计算问题,转变为调度问题,增加了其他线程的等待时间,同时线程之间的TCB切换等工作也会耗时



问题:计算密集型应用,那线程一般建议创建多少个?

答:CPU物理个数乘于核数,相当于在多个多核处理器系统上,能够将所有CPU核心都刚好运用上



理想情况:线程数 = CPU核心数

理论基础:在理想的计算密集型应用中,每个线程都应该绑定到一个独立的CPU核心上,以避免上下文切换和资源竞争。因此,线程的数量应等于系统中的物理核心数(或逻辑核心数,如果启用了超线程技术)。

公式:

单个处理器:线程数 = 物理核心数

多核处理器:线程数 = 物理核心数 × 处理器数量

超线程技术:如果启用了超线程(Hyper-Threading),则线程数 = 逻辑核心数 = 物理核心数 × 2

示例:
如果你有一个4核的处理器,且没有启用超线程,那么理论上你应该创建4个线程。
如果你有一个8核的处理器,并且启用了超线程,那么你应该创建16个线程(8核 × 2)。



问题:什么是超线程技术?


超线程技术,每个物理核心可以模拟出两个逻辑核心。理论上,你可以创建两倍于物理核心数的线程。

超线程技术(Hyper-Threading Technology, HTT) 是英特尔(Intel)开发的一种并行计算技术,旨在通过让每个物理CPU核心模拟出多个逻辑核心(通常为两个),从而提高多线程应用程序的性能。超线程技术的核心思想是利用单个物理核心中的闲置资源来同时执行多个线程,从而提高系统的整体效率。

  • 物理核心 vs. 逻辑核心
    • 物理核心:每个物理核心是一个独立的处理单元,具备自己的算术逻辑单元(ALU)、寄存器、缓存等资源。
    • 逻辑核心:在启用超线程的情况下,每个物理核心可以模拟出两个或更多的逻辑核心。操作系统会将这些逻辑核心视为独立的处理器,并可以为每个逻辑核心分配不同的线程。
  • 资源共享
    • 在超线程技术中,物理核心的某些资源(如执行单元、缓存、浮点运算单元等)是共享的,而其他资源(如寄存器、指令队列等)则是为每个逻辑核心独立分配的。
    • 这意味着,虽然每个逻辑核心可以独立执行线程,但它们仍然共享同一物理核心的大部分硬件资源。因此,超线程并不能真正增加物理核心的数量,而是通过更高效地利用现有的资源来提高性能。

这就是为什么我的笔记本电脑有8个内核,却有16个逻辑处理器的原因



拓展:多核处理器与多处理器系统


处理器等同于CPU吗?

在广义定义上,处理器是一个更为广泛的概念,指的是任何能够执行指令并处理数据的电子设备或组件。它不仅包括中央处理器(CPU),还可以指其他类型的处理器,如图形处理器(GPU,专门用于图形渲染和并行计算)、张量处理单元(TPU,专为机器学习任务设计的处理器)等。



多核处理器与CPU的关系

  • 多核处理器:指在一个物理芯片上集成了多个CPU核心。每个核心都可以独立执行程序指令,相当于一个单独的CPU。因此,可以将多核处理器视为多个CPU核心集成在一起的处理器。
  • CPU核心:每个CPU核心都是一个完整的计算单元,能够进行指令解码、执行运算以及访问内存等操作。


多核处理器和多处理器系统的区别

  • 多核处理器

    • 在一个物理芯片上集成多个独立的CPU核心。
    • 所有核心共享同一块物理芯片上的资源,例如内存控制器、总线等。
  • 多处理器系统

    • 指一台计算机中安装了两个或更多的物理处理器(CPU芯片)。这些处理器可以是单核或多核的。
    • 每个处理器(无论是单核还是多核)都有自己的缓存、内存控制器等资源,并且它们之间需要有效的通信机制来协调工作。



2、线程的缺点

  • 性能损失
    • 一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 健壮性降低
    • 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
  • 缺乏访问控制
    • 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高
    • 编写与调试一个多线程程序比单线程程序困难得多。这个主要是因为线程共享资源,容易起冲突,就需要程序员设置各种保护机制。


3、线程异常


(1)进程级别的致命信号触发

  • 当一个线程因非法操作(如访问无效内存地址)导致触发致命信号(例如SIGSEGV),这会导致整个进程终止。这是因为大多数操作系统将这类信号视为进程级别的事件,而不是单独针对产生问题的线程。因此,一旦某个线程引发这种类型的崩溃,该进程下的所有线程都会被终止。

(2)未处理的算术和内存错误导致进程崩溃

  • 如果单个线程执行了除零操作或使用了野指针,且这些行为未被捕获处理,则会引发运行时错误。在C/C++等语言中,这种情况通常会导致程序直接崩溃。如果此线程所在的进程没有设置适当的信号处理器来处理此类异常情况,那么该进程将会终止,同时其所有的线程也会随之结束。

(3)异常处理与信号机制

  • 线程作为进程的执行分支,在出现异常时首先会在其自身的上下文中尝试处理。然而,对于一些无法恢复的严重错误(比如访问非法内存地址、除零等),如果没有有效的异常处理机制,会触发相应的信号(例如SIGFPE用于算术异常,SIGSEGV用于无效内存访问)。由于这些信号默认是发送给整个进程的,而非特定线程,因此会导致整个进程终止,包括其中的所有线程。

(4)注意事项

有人说:“一个线程崩溃,其他同进程下的所有线程都会被干掉,系统会给这些线程都发相同信号。”

这句话实际上包含了几个误解点:

错误点一:一个线程崩溃,其他同进程下的所有线程都会被干掉

纠正说明:

  • 当一个线程因执行非法操作(例如访问无效内存地址或除零)导致崩溃时,并不是直接导致其他线程被“干掉”。真正发生的是该线程触发了一个致命信号(如SIGSEGV)。由于大多数操作系统将此类信号视为进程级别的事件,这意味着一旦某个线程引发了这种类型的错误,整个进程可能会因此终止。而当进程终止时,属于该进程的所有线程也会随之结束。

错误点二:系统会给这些线程都发相同信号

纠正说明:

  • 实际上,在遇到上述情况时,信号是发送给整个进程的,而不是单独发送给每个线程。在Unix/Linux等操作系统中,许多关键信号(如SIGSEGV, SIGFPE)是针对整个进程的。如果一个线程触发了这样的信号且未被捕获处理,则该信号会导致整个进程终止,而非仅仅影响到产生问题的那个线程。换句话说,没有所谓的系统直接向其他线程发送相同的信号来“干掉”它们,而是进程的终止间接导致了所有线程的结束。


(5)系统如何干掉同进程下的所有线程?

是按照线程组为单位杀死线程的。

在进程里存在线程组的概念,线程组中包含了本进程下的所有线程,而线程组的ID值为TGID。主线程(也就是第一个启动的线程)的TID等于该线程组的TGID。当某个线程触发某些异常情况导致系统发送致命信号给此进程时,在进程退出前,系统会以线程组为单位,将属于该进程的所有线程一并清除并退出。这样做确保了整个进程要么完全运行,要么彻底停止,有效地避免了由于部分线程残留可能引发的问题。



(6)有没有发送给线程的信号

确实存在可以发送给线程的信号,但这种情况与发送给进程的信号有所不同。在Unix/Linux系统中,信号通常被设计为发送给整个进程。然而,随着多线程编程的发展,也引入了对特定线程发送信号的能力。

发送给线程的信号

  • pthread_kill() 函数:在支持POSIX线程(pthreads)的系统中,可以使用pthread_kill()函数向特定的线程发送信号。这允许程序对特定线程进行更加精细的控制。例如,可以通过这种方式来中断一个长时间运行的线程而不影响整个进程。

  • 线程特定信号处理:当信号发送到特定线程时,只有目标线程会调用其注册的信号处理程序。这意味着可以在不影响其他线程的情况下单独处理某个线程中的异常情况或其他需要关注的状态变化。

综上所述,虽然大多数情况下讨论的信号都是以进程为中心的,但在多线程环境中,确实也有直接发送给特定线程的信号,并且这种机制提供了更细粒度的控制能力。



4、线程用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com