理解Go的GMP模型(一)——进程、线程、协程和调度器

理解Go的GMP模型(一)——进程、线程、协程和调度器

  • 作者:晓白齐齐
  • 更新:2024.1.11
  • 声明:本文用于记录作者学习过程的心得理解,限于作者水平有限,可能存在理解错误,欢迎提出。参考文档一栏列出了本文参考的文档,本文的一些图表来自这些参考文档,如有侵权,联系作者删除,还请见谅。为避免转载导致的文章质量下降,本文禁止转载。

一、进程

1.单进程

单进程时代的操作系统运行完一个进程才能进行下一个进程,一切程序只能串行运行,不需要调度器。存在的问题:

  1. 计算机只能一个任务一个任务处理,不能同时处理多个任务。
  2. 某个进程阻塞,CPU 无法切换到另一个进程,导致 CPU 阻塞期间空闲而浪费 CPU 时间资源。

pl2C28QoF8.png!large

图1. 单进程时代操作系统

2.多进程和调度器

为了解决单进程的问题,出现了多进程操作系统,当一个进程阻塞, CPU 空闲时,就切换到另外等待执行的进程,尽可能把 CPU 利用起来,减少 CPU 的空闲时间。

这个切换过程由 CPU 进程调度器实现的,这样就实现了多个进程宏观上同时运行。进程调度器本身也是一个进程,这是一个操作系统级别实现的进程。

M3nVkPhLTC.png!large

图2. 多进程操作系统通过调度器将 CPU 时间分配给多个进程

3.进程的内存

程序启动时,操作系统会给进程分配一块内存空间,对于程序员来说,这块空间是连续的,但实际上这是一块虚拟内存,对于操作系统内核来说,这块空间是通过一个页表(记录了虚拟空间和内存空间的映射关系)映射到物理内存中一块一块的内存碎片。

Fsogk5CdNR.jpg!large

图3. 操作系统分配给程序的虚拟内存实际上是通过映射表映射到零碎的物理内存


进程调度器切换进程时需要进行页表切换、虚拟地址空间切换,这个切换过程需要保存寄存器、栈、代码段、执行位置等进程的现场信息,以便下次进程切换回来恢复现场,这个操作需要耗费大量的 CPU 开销。


二、线程

1.线程和多线程

为了解决进程切换耗费的大量 CPU 开销,线程出现了。程序运行起来后,操作系统会为该程序分配一块内存空间,并调度 CPU 来执行程序。

单进程操作系统串行运行进程,进程同时拥有内存空间和 CPU 时间,进程结束则内存空间和 CPU 同时被操作系统收回。

多进程时期,操作系统为一个进程分配内存空间后,这块内存空间就会被进程一直拥有直至进程结束,而 CPU 是在不同进程之间切换。一个进程不一定同时拥有 CPU 时间和内存空间。

对操作系统来说资源分配(内存分配)和 CPU 调度是两个不同的单位,因此引入了线程,进程是资源分配(内存分配)的最小单位,线程是 CPU 调度的最小单位。

一个进程分成多个执行功能的线程,操作系统分配资源给进程,调度 CPU 时间给线程,实现了多个进程宏观上同时运行,就是多线程。线程之间的调度则由线程调度器来实现,只有拥有了线程的进程才会被 CPU 执行,一个进程至少拥有一个主线程。

1JxtDUEF5X.jpg!large

图4. 一个进程一直拥有一块内存空间,由多个线程组成


只有在程序运行时才有进程和线程两个概念,程序在没有运行时只是一个可执行的二进制文件。单进程时期没有线程的概念,为了单进程的问题,引入了线程和进程/线程调度器,调度的就是 CPU 时间。

2.线程的内存

操作系统以进程为单位分配内存空间,因此同一进程的多个线程共享该进程的内存空间。线程的创建不需要新建虚拟空间,线程之间的切换也不用像进程一样切换页表、虚拟地址空间,只需要保存程序计数器等线程的执行现场, CPU 把栈指针和指令寄存器指向下一个线程即可实现线程切换,因此,线程的切换开销比进程小得多。

3.多线程并发的问题

多线程操作系统通过 CPU 调度器保证多个线程都可以分配到 CPU 运行时间。但是仍然存在一些问题,例如:如果多个线程同时访问同一块内存,有可能造成不可预计的异常;如果一个线程长时间霸占了 CPU ,调度算法又没有对这种情况做出处理,就会造成其他线程永远得不到执行。为了解决这些问题,多线程开发使得设计变得更复杂。

虽然多线程提高了系统的并发能力,线程的切换对内存和 CPU 的消耗也低于进程的切换,但是在当下互联网高并发的要求下,为每个任务创建一个线程是不现实的,多线程带来的性能提升犹显不足。

总结来说,多线程虽然解决了单进程操作系统的问题,但又带来新的问题:

  1. 一个线程拥有太多资源,线程的创建、切换、销毁这个过程又会占用 CPU 的很多时间。
  2. 大量线程并发运行, CPU 的很大部分时间都是用来调度线程的。
  3. 一个线程至少需要消耗超过 1MB 的内存,大量线程将导致高内存的占用。
  4. 多线程使得开发设计变得更复杂,容易出问题。

OgVa2m4vMJ.png!large

图5. 多线程并发时大部分时间都用来进行线程切换

4.内核空间和用户空间,“内核态线程”和“用户态线程”

前面说到操作系统会为一个进程分配一块内存空间。这块内存空间又分为用户空间和内核空间,用户空间用于程序的运行,而内核空间则用于执行操作系统的系统调用,当需要执行各种 IO 操作时,通过系统调用进入内核空间进行操作。

与之对应,一个线程便可分为“内核态线程”和“用户态线程”,一个“用户态线程”必须绑定一个“内核态线程”, CPU 并不知道“用户态线程”的存在,只知道它运行的是一个“内核态线程”。

0ejFovRhea.png!large

图6. 一个线程分为用户线程和内核线程,分配的内存空间分为用户空间和内核空间


三、协程

线程的创建销毁由内核来做,因此造成的系统开销仍然是比较大的,通过线程池可以解决这个问题。另一种解决方法就是这节要讲的协程。

1.协程

前面提到了“用户态线程”,它需要绑定一个“内核态线程”才可以运行。“用户态线程”有一个耳熟能详的名字,就是协程,而“内核态线程”仍然叫做“线程”。

UnI5IH27eT.png!large

图7. 一个线程绑定一个协程,协程运行在用户空间,线程运行在内核空间


协程比线程更轻量,一个协程需要绑定一个线程才能运行,操作系统对其没有感知,协程处于线程的用户栈能够感知的范围,由程序员创建而非操作系统。

和一个进程可以拥有多个线程类似,一个线程也可以被多个协程绑定。将多个协程绑定到一个或者多个线程上,便可实现线程的复用,节省了创建线程的内存空间和切换线程的开销。

aSlo0fAyUA.jpg!large

图8. 一个线程绑定多个协程,实现线程的复用

2.协程和线程的三种映射关系

协程 : 线程 优点 缺点 图示
N : 1 协程在用户态线程即完成切换,不会陷入到内核态,切换非常轻量快速 1. 1个进程的所有协程绑定在一个线程上; 2. 使用不了硬件的多核加速能力; 3. 一旦某个协程阻塞,线程也就阻塞了,其他协程就无法执行,失去了并发的能力。 图9所示
1 : 1 协程的调度都由 CPU 实现,容易实现 协程的创建、删除、切换都由 CPU 完成,性能消耗大,与线程调度无异 图10所示
M : N N : 1 和 1 : 1 的结合,克服了前两者的缺点 实现起来最为复杂 图11所示

表1. 协程和线程的三种映射关系

HlLBcLAbID.png!large

图9. 一个线程绑定N个协程


YLXvQeMcDN.png!large

图10. 一个线程绑定1个协程


llujvJarm4.png!large

图11. M个线程绑定N个协程

3.协程的调度

线程拥有自己的协程队列,每个协程拥有自己的栈空间。不同协程在线程之间的切换操作,由协程调度器实现。与进程/线程调度器不同,协程调度器是用户实现的,而非操作系统内核实现。

线程由 CPU 抢占式的调度,而协程由用户进行协作式的调度,一个协程让出 CPU 后,才执行下一个协程。


四、调度器

前面3节都出现了调度器,调度器实际上也是一个进程/线程/协程,都是调度 CPU 的使用,创建、销毁、切换 CPU 的占用单元。进程调度器调度进程,需要保存页表、虚拟内存地址等进程执行现场,并在切换进程时恢复执行现场。由于进程调度器的调度进程的消耗过大,线程出现了,线程调度器调度线程这种比进程更轻量的执行单位。当线程也不足以支持需求时,协程出现了。

不同的是,进程/线程调度器是操作系统内核实现的, CPU 能够感知进程/线程的存在,且都是抢占式的调度。而协程是由用户实现的, CPU 感知不到协程的存在,属于协作式调度。


总结

  1. 单进程时代一个进程运行完才能进入下一个进程,不需要调度器,也没有线程、协程的概念。
  2. 程序运行时操作系统会分配一块连续的虚拟内存给进程,这块内存通过页表映射到物理内存中一块一块的零碎内存。
  3. 从操作系统层面,进程是资源分配(主要是内存)的最小单位,线程是 CPU 调度的最新单位。只有在程序运行时才有进程和线程的概念。
  4. 一个进程由一个或者多个线程组成,只有包含线程的进程才能被 CPU 调度,一个进程至少应包含一个主线程。
  5. 进程分配到的内存空间分为用户空间和内核空间,用户空间用于执行用户程序,内核空间用于发生系统操作(如各种 IO 操作)时,进行系统调用进入内核内存空间进行操作。与内存空间对应的,一个线程分为“用户态线程”和“内核态线程”,“用户态线程”运行在用户空间,“内核态线程”运行在内核空间。
  6. “用户态线程”即常提到的协程, CPU 对协程无感知,一个协程必须绑定到一个线程才能运行。
  7. 协程由程序员创建而非操作系统内核创建。
  8. 进程调度器在切换进程时需要进行页表切换、虚拟地址空间切换,记录寄存器、栈、代码段、执行位置等现场信息,线程调度器在切换线程时需要记录程序计数器,移动 CPU 的栈指针和指令寄存器;协程调度器切换协程时记录协程的上下文;三种调度器的消耗一个比一个低,三种调度单位一个比一个轻量。
  9. 用户内存空间用于执行程序,内核内存空间用于在发生 IO 时,进行系统调用进入内核内存空间进行操作。
  10. 进程/线程之间的调度由 CPU 调度器执行,进行的是抢占式调度,调度器本身也是一个进程/线程;协程调度器由用户实现,进行的是协作式调度,一般来说协程调度器本身也是一个协程。

参考文档


本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!