零基础快速入门C#并发编程:线程基础(1) -- 线程与进程基础概念

56 阅读7分钟

注意:此文中的 OS,如无特殊说明,指的是 Window 操作系统;

1.1 线程与进程基础概念

什么是进程

在 Window 当中,一个进程中允许应用程序的每一个实例;

  • 进程基本特点
    • 进程实际上是应用程序的实例要使用的资源的集合
    • 进程的隔离性
      • 每个进程都被赋予了一个虚拟地址空间,确保在一个进程中使用的代码和数据无法由另一个进程访问;
      • 这确保了应用程序实例的健壮性因为一个进程无法破坏另一个进程使用的代码或数据
    • 对于 OS 内核的隔离
      • 进程访问不了 OS 的内核代码和数据
      • 因此应用程序代码破坏不了操作系统代码或数据,系统变得比以往更安全;

什么是线程

进程对于要使用的资源进行了虚拟化,通过隔离提供了虚拟地址空间; 线程对于 CPU 进行了虚拟化,对于代码的执行会以一个个的线程为单位来获得 CPU 时间片资源;

  • 线程的职责是对 CPU 进行虚拟化;
    • Windows 为每个进程都提供了该进程专用的线程,当应用程序的代码进入死循环,与那个代码关联的进程会“冻结”,但其他进程 (它们有自己的线程)不会冻结,它们会继续执行!

1.2 线程的开销

线程所带来的开销主要分为两个方面:

  1. 空间上带来的开销;比如为每个线程提供的栈、内核对象等;
  2. 时间上带来的开销;比如线程进行上下文切换时会进入到 OS 内核代码,消耗 CPU 时间片;

1.2.1 线程的空间开销

开销一:线程内核对象

OS 为每个线程都分配并初始化这种数据结构之一;

  • 包含一组对线程进行描述的属性 ;
  • 包含线程上下文
    • 上下文是包含 CPU 寄存器集合的内存块;
    • 对于 x 86,x 64 和 ARM 的 CPU 架构线程上下文分别使用约 700,1240 和 350 字节的内存;

开销二:线程环境块

线程环境块:TEB

  • TEB 是在用户地址空间中分配和初始化的内存块;
  • TEB 耗用 1 个内存页 (x 86 x 64 和 ARM 的 CPU 中是 4 KB);
  • 主要构成
    • 异常处理
      • EB 包含线程的异常处理链首 (head),线程进入的每个 try 块都在链首插入一个节点 (node)
      • 线程退出 try 块时从链中删除该节点
    • 线程本地存储
      • OpenGL 图形,以及 GDI 图形接口;

开销三:用户模式栈

在用户地址空间下执行的程序代码需要使用用户模式栈,来传递程序的局部变量、参数等信息;

  • 用户模式栈存储传给方法的局部变量和实参;
    • 它还包含一个地址,指出当前方法返回时线程应从什么地方接着执行;
    • Windows 默认为每个线程的用户模式栈分配 1 MB 内存;

开销四:内核模式栈

应用程序代码向操作系统中的内核模式函数传递实参时,会使用内核模式栈;

  • 内核模式栈特点
    • 栈数据复制
      • 针对从用户模式的代码传给内核的任何实参,Windows 都会把它们从线程的用户模式栈复制到线程的内核模式栈;
      • 一经复制,内核就可验证实参的值
    • 应用程序代码不能访问内核模式栈
      • 应用程序无法更改验证后的实参值
    • 在 32 位 Windows 上运行,内核模式栈大小是 12 KB;64 位 Windows 是 24 KB;

1.2.2 线程的时间开销

开销一:DLL 线程链接和分离的通知 在 Windows 当中,当在进程中创建线程时,将会调用进程中加载的所有非托管 DLL 的 DIIMain 方法,并传递 DLL_THREAD_ATTACH 标志; 在 Windows 当中,当在进程中终止线程时,将会调用进程中加载的所有非托管 DLL 的 DIIMain 方法,并传递 DLL_THREAD_DEATTACH 标志;

开销二:上下文切换 CPU 的数量经常会少于线程的数量,但 Windows 在任何时刻只会讲一个线程对应分配给一个 CPU,按照时间片为单位使用一段时间 CPU,然后切换给其他的线程继续执行; 所以,需要在一个线程执行完后进行上下文切换,使得 Windows 进入到另一个线程继续执行;

  • 上下文切换实现原理
    • 第一步:
      • 将 CPU 寄存器的值保存到当前正在运行的线程的内核对象内部的一个上下文结构中;
    • 第二步:
      • 从现有线程集合中选出一个线程供调度;
      • 如果该线程由另一个进程拥有,Windows 在开始执行任何代码或者接触任何数据之前,还必须切换 CPU 使其“看见”的虚拟地址空间;
    • 第三步:
      • 将所选上下文结构中的值加载到 CPU 的寄存器中;

开销三:垃圾回收与线程开销 执行垃圾回收时,CLR 必须挂起所有线程,遍历它们的栈来查找根,以便对堆中的对象进行标记; 再次遍历它们的栈后 (有的对象在压缩期间发生了移动,所以要更新它们的根),再恢复所有线程;

  • 程越多,调试体验越差。
    • 减少线程的数量也会显著提升垃圾回收器的性能;
    • 每次使用调试器并遇到断点,Windows 都会挂起正在调试的应用程序中的所有线程,并在单步执行或者运行应用程序时恢复所有线程;

1.3 线程其他基础概念

1.3.1 线程数与 CPU 数量

理论上,任何机器最优的线程数就是机器的 CPU 数目

注意:这里是将 CPU 的每一个内核,都当作一个 CPU;

当线程数超过了 CPU 数,就会发生上下文切换,进而产生更多的性能损失; 但 OS 的设计当中,需要权衡两个方面:

  • 方面一:可靠性与响应速度;
  • 方面二:执行速度与性能; OS 的设计者需要权衡两者;

因为创建进程的代价昂贵,因此开发人员会选择创建大量的线程

  • 在 Windows 中创建进程十分昂贵
    • 创建一个进程通常要花几秒钟的时间,必须分配大量内存;
    • 这些内存必须初始化,EXE 和 DLL 文件必须从磁盘上加载等等;
  • 在 Windows 中创建线程十分廉价
  • 开发人员决定停止创建进程,改为创建线程。但虽然线程比进程廉价,它们和其他系统资源相比仍然十分昂贵,所以还是应该省着用,而且要用得恰当;

1.3.2 CPU 发展趋势

早期阶段:提升 CPU 速度 通过提升单 CPU 速度,提供程序运行效率;

  • 问题:
      1. 高速运行的 CPU 产生热量大;
      1. 不能做到一直提升单 CPU 的速度;

后背的发展:多 CPU 内核 将晶体管做得更细,进而一个芯片容纳更多晶体管,同时容纳更多的 CPU 内核;

多 CPU 技术

  • 多 CPU
  • 超线程芯片
  • 多核 CPU

1.3.3 线程的优势

好处一:可响应性

  • Windows 为每个进程提供其自己的线程。对于一些 GUI 程序而言也是如此,可以将一些需要响应 IO 的工作交给一个专门的线程来进行反应,使得 GUI 线程可以灵敏的反应用户的输入。
  • 对应的缺点
    • 可能会导致线程数爆炸,太多的线程占据内存;

好处二:性能

  • 在多核 CPU 下,更多的线程可以分配到不同的 CPU 上,并行进行计算,提高操作性能;