Gmp

2021/08/03

GMP

GMP 是 Golang 的调度器实现方案,其实不难,带着以下几个问题去思考?

  • Q1:进程、线程是什么?它们之间的关系?
  • Q2:协程是什么?它和进程、线程的关系是什么?
  • Q3:Golang 旧版调度器是什么?有什么缺点?
  • Q4:G、M、P 分别指代什么?
  • Q5:为什么要有 P?解决了什么问题?
  • Q6:G、M、P 的数量关系应该是如何的?
  • Q7:G、M、P 到底是怎么调度的(关键)?

上面所有问题的答案都可以在 典藏版 Golang 调度器 GMP 原理与调度全分析 ,下面的内容将是简洁的回答。

Q1:进程、线程是什么?它们之间的关系?

进程是操作系统对一个正在运行的程序的一种抽象,进程是资源分配的最小单位。系统是给进程直接分配资源,它在运行过程中持有相关上下文(寄存器)。

进程存在的意义是:CPU速度太快,而单个任务(job)不一定实时的使用CPU,可能会等待其他IO,此时对于CPU就是一种浪费,所以形成了一个新的使用场景。多个任务同时运行,按照不同的CPU调度策略进行分配,在计算机科学中,单个的任务就叫做进程。

进程是一个缩写,代表:进行的程序。所以对于大部分程序来说,都是只启动一个进程的,也可以说一个进程就是一个实例。它的粒度是比较大的,程序内部的代码是有多个分支或者程序段的,这时就引出了线程概念。线程是依托于进程的,它们之间可以共享进程所持有的上下文信息,对于关键资源还会通过锁的方式进行线程间的限制。线程运行时的本质和进程一样,都是要占据 CPU 一段时间进行运算。

进程是资源分配的最小单位,线程是 CPU 调度的最小单位。

做个简单的比喻:进程=火车,线程=车厢

  • 线程在进程下行进(单纯的车厢无法运行)
  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)
  • 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
  • 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
  • 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
  • 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
  • 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
  • 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-”互斥锁”
  • 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”

Q2:协程是什么?它和进程、线程的关系是什么?

协程其实不算是操作系统的概念,不属于内核层,它更多的是编程层面的概念,属于用户态(即协程是由编程语言实现,由开发者自己创建并调用)。

协程与进程、线程相比并不是一个维度的概念,协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。

Q3:Golang 旧版调度器是什么?有什么缺点?

GM 模型,G是协程,M是线程。M 想要执行、放回 G 都必须访问全局 G 队列,并且 M 有多个,即多线程访问同一资源需要加锁进行保证互斥 / 同步,所以全局 G 队列是有互斥锁进行保护的。

缺点:

  • 创建、销毁、调度 G 都需要每个 M 获取锁,这就形成了激烈的锁竞争。
  • M 转移 G 会造成延迟和额外的系统负载。比如当 G 中包含创建新协程的时候,M 创建了 G’,为了继续执行 G,需要把 G’交给 M’执行,也造成了很差的局部性,因为 G’和 G 是相关的,最好放在 M 上执行,而不是其他 M’。
  • 系统调用 (CPU 在 M 之间的切换) 导致频繁的线程阻塞和取消阻塞操作增加了系统开销。

Q4:G、M、P 分别指代什么?

GMP 在 Golang 里就是三种数据结构,它们的功能是:

  • G: goroutine,协程
  • M:thread,线程
  • P:Processor,处理器

Q5:为什么要有 P?解决了什么问题?

P 负责 G 和 M 之间的调度。一般来说一个 P 必定会绑定一个 M,同时持有一个队列,这样新创建的 G 就会加入到 P 的队列中,接着被 M 执行。解决了 GM 模型中的三个缺点:锁竞争,资源消耗高,线程阻塞。

Q6:G、M、P 的数量关系应该是如何的?

G 和 M 都有一个全局队列,G 和 M 在全局队列的时候就是睡眠状态。P 的数量是一定的,通过启动时环境变量 $GOMAXPROCS 或者是由 runtime 的方法 GOMAXPROCS() 决定。在一般情况下,(不考虑全局队列中睡眠的M),P 和 M 的数量是 1:1,而 G 和 P 的数量是 N:1。

Q7:G、M、P 到底是怎么调度的(关键)?

实际的模型中,为了线程(M)的复用,有以下两个机制:

  • work stealing 机制:当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。
  • hand off 机制:当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。

先明确整个调度的框架:多个 M、每个 M 绑定一个 P,每个 P 拥有一个 G 队列,除此之外还有全局的 G 队列和 M 队列。

  • 对于一个新产生的 G,它默认会出现在 spawn 该 G 的 G 的 M 对应的 P 拥有的 G 队列,如果队列满了,队列会把一半的 G 放到全局队列中,把当前 G 插入。
  • 如果 G 队列空了,那么 P 会尝试从全局 G 队列拿,如果没有那就尝试着去其他 P 队列偷。
  • 如果 M 执行一个 G,但此时堵塞了,那么 P 不会等待,它会尝试从全局 M 队列获取睡眠的 M,如果没有就创建一个 M,然后该 P 绑定上 M。那个阻塞的 M 在完成任务后会进入到全局 M 队列。

参考

Search

    Table of Contents