2.7 说一下 GMP 模型的原理 ========================= 1. 什么是 GMP ? ---------------- - ``G``\ :Goroutine,也就是 go 里的协程,是用户态的轻量级线程,具体可以创建多个 goroutine ,取决你的内存有多大,一个 goroutine 大概需要 4k 内存,只要你不是在 32 位的机器上,那么创建个几百万个的 goroutine 应该没有问题。 - ``M``\ :Thread,也就是操作系统线程,go runtime 最多允许创建 10000 个操作系统线程,超过了就会抛出异常 - ``P``\ :Processor,处理器,数量默认等于开机器的cpu核心数,若想调小,可以通过 GOMAXPROCS 这个环境变量设置。 2. GMP 核心 ----------- 两个队列 ~~~~~~~~ 在整个 Go 调度器的生命周期中,存在着两个非常重要的队列: - 全局队列(Global Queue):全局只有一个 - 本地队列(Local Queue):每个 P 都会维护一个本地队列 当你执行 ``go func()`` 创建一个 goroutine 时,会优选将该协程放入到当前 P 的本地队列中等待被 P 选中执行。 但若当前 P 的本地队列任务太多了,已经存放不下了,那么这个 goroutine 就只能放入到全局队列中。 两种调度 ~~~~~~~~ 一个协程得以运行,需要同时满足以下两个条件: 1. P 已经和某个线程进行绑定,这样才能参与操作系统的调度获得 CPU 时间 2. P 已经从队列中(可以是本地队列,也可以是全局队列,甚至是从其他 P 的队列)取到该协程 第一个条件就是 **操作系统调度**\ ,而第二个其实就是 **Go 里的调度器**\ 。 操作系统调度 ^^^^^^^^^^^^ 假设一台机器上有两个 CPU 核心,意味着,同时在同一时间里,只能有两个线程运行着。 可如果该机器上实际开启了 4 个线程,要是先执行一个线程完再执行另一个线程,那么当某一个线程因为一些阻塞性的系统调用而阻塞时,CPU 的时间就会因此而白白浪费掉了。 更合适的做法是,使用 **操作系统调度策略**\ ,设定一个调度周期,假设是 10ms (毫秒),那在一个周期里,每个线程都平均分,都只能得到 2.5ms 的CPU 运行时间。 可如果机器上有 1000 个线程呢?难道每个线程都分个 0.01 ms (也就是 10 微秒)吗? 要知道,CPU 从 A 线程切换到 B 线程,是有巨大的时间浪费在线程上下文的切换,如果切换得太频繁,就会有大量的 CPU 时间白白浪费。 |image0| 因此,通常会限制最小的时间片的长度,假设为 2ms,受此调整,现在调度周期就会变成 2*1000 = 2s 。 Go调度器 ^^^^^^^^ 在 Go 中需要用到调度的,无非是如下几种: **将 P 绑定到一个合适的 M** P 本身不能直接运行 G,只将 P 跟 M 绑定后,才能执行 G。 假设 P1 当前正绑定在 M1 上运行 G1,此时 G1 内部发生了一次系统调度后,P1 就会与 M1 进行解绑,然后再从空闲的线程队列中再寻找一个来绑定,假设绑定的是 M2,可如果没有空闲的线程呢?那没办法,只能创建一个新的线程再进行绑定。 绑定后,就会再从本地的队列中寻找 G 来执行(如果没找到,就会去其他队列找,上面已经讲过,不再赘述)。 过了一段时间后,之前 M1 上 G1 发生的系统调用结束后,M1 会去找原先自己的搭档 P1(它自己会记录),如果自己的老搭档也刚好空闲着,就可以再次合作进行绑定,接着运行 G1 未完成的工作。 可不幸的是,P1 已经找到了新的合作伙伴 M2,暂时没空搭理 M1 。 M1 联系不上 P1,只能去寻找有没有其他空闲的 P ,如果所有的 P 都被绑定着,说明现在任务非常繁重,想完成任务只能排队慢慢等。 于是,M1 上的 G1 就会被标记为 Runable ,放到全局队列中,而 M1 自身也会因为没有 P 可以绑定而进入休眠状态,如果长时间休眠等待 则会 GC 回收销毁 **为 P 选中一个 G 来执行** P 就像是一个流水线工人,而 P 的本地队列就是流水线,G 是流水线上的零件。而 Go 调度器就是流水线组长,负责监督工人的是不是有在努力的工作。 完成一个 G 后,P 就得立马接着从队列中拿到下一个 G,继续干活。 遇到手脚麻利的 P ,干完了自己的活,本想着可以偷懒一会,没想到却被组长发现了,立马就从全局队列中拿了些新的 G 交到 P 的手里。 天真的 P 以为只要把 全局队列中的 G 的也干完了,就肯定 能休息了吧? 当 P 又快手快脚的把全局队列中的 G 也都干完的时候,P 非常得意,心想:终于可以休息会了。 没想到又被眼尖的组长察觉到了:不错啊,小 P,手脚挺麻利的。看看其他人,还那么多活没干完。真是拖后腿。可谁让咱是一个团队的呢,要有集体荣誉感,你能者多劳。 说完,就把其他人的 G 放到了我的工作上。。。 3. 调度器的设计策略 ------------------- 复用线程 ~~~~~~~~ 避免频繁的创建、销毁线程,而是对线程的复用。 **1)work stealing 机制** 当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。 **2)hand off 机制** 当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。 利用并行 ~~~~~~~~ GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。GOMAXPROCS 也限制了并发的程度,比如 GOMAXPROCS = 核数/2,则最多利用了一半的 CPU 核进行并行。 抢占调度 ~~~~~~~~ 在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死,这是协作式抢占调度。 而在 go 1.14+ ,Go 开始支持基于信号的真抢占调度了。 全局 G 队列 ~~~~~~~~~~~ 在新的调度器中依然有全局 G 队列,但功能已经被弱化了,当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G。 延伸阅读 -------- - `[典藏版] Golang 调度器 GMP 原理与调度全分析 `__ .. |image0| image:: http://image.iswbm.com/20210904140447.png