目录
一、Go 语言的协程 goroutine
1. goroutine概念
goroutine 来自协程的概念,让一组可复用的函数运行在一组线程之上;
即使有协程阻塞,该线程的其他协程也可以被 runtime 调度,转移到其他可运行的线程上;
2. goroutine特点
占用内存更小(只占几 KB,可伸缩的,runtime会自动为goroutine分配更多内存);
调度更灵活 (runtime 调度);
二、m:n调度技术
m:n调度的技术,m个goroutine(G)调度到n个OS线程(M)上运行;
一对一和一对多的缺点;
三、goroutine 调度器的实现
1. 被废弃的调度器
2012年重新设计之前的调度器,只有GM;
特征:
1)M 想要执行、放回 G ,都必须访问全局 G 队列;
2)M 有多个,即多线程访问同一资源(即G队列)需要加锁进行保证互斥 / 同步。
老调度器有几个缺点:
1)激烈的锁竞争;
2)M 转移 G 会造成延迟和额外的系统负载;
比如当 G 中包含创建新协程的时候,M 创建了 G’;为了执行 G,需要把 G’交给 M’,也造成了很差的局部性,因为 G’和 G 是相关的,最好放在 M 上执行,而不是其他 M’;
3)系统调用 (CPU 在 M 之间的切换) 导致频繁的线程阻塞和取消阻塞操作增加了系统开销;
2.新调度器GPM设计思想
GPM是Go语言运行时层面(用户态)的实现,是go语言自己实现的一套调度系统,区别于操作系统调度OS线程(内核态)。
1)G:Goroutine,协程;
包含goroutine信息和与P的绑定信息;
2)P:Processor,调度器;
所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个;
管理和调度一组goroutine队列,当自己的队列消费完了去全局队列里取,全局队列里消费完了会去其他P队列里取;
2)M:Machine,线程映射;
Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系;
线程想运行任务就得获取 P,从 P 的本地队列获取 G;
4)GQ:全局队列(Global Queue);
5)LQ:P 的本地队列;
存的数量有限,不超过 256 个;
满:新建 G’时,G’优先加入到 P 本地队列,如果队列已满,则把队列中前一半的 G和新创建的G移动到全局队列;
空:P 队列为空时,M 尝试从全局队列拿一批 G 【负载均衡,min(全局队列长度/P的数量 + 1, 全局队列长度/2)】放到 P 本地队列,或从其他 P 本地队列后一半放到自己 的P 本地队列;
协程和线程是通过P结合起来的(G - P -M), 线程想运行任务就得获取 P,从 P 的本地队列获取 G;
Goroutine 调度器和 OS 调度器是通过 M 结合起来的(goroutine调度器 - M - OS调度器);
3.P和M的数量和创建
数量
1)P 的数量:
由GOMAXPROCS决定,可配置;
这意味着在程序执行的任意时刻都只有 $GOMAXPROCS 个 goroutine 在同时运行;
2)M 的数量:
go 语言限制:go 程序启动时,会设置 M 的最大数量,默认 10000,但是内核很难支持这么多的线程数;
SetMaxThreads 函数,可设置 M 的最大数量;
一个 M 阻塞了,会创建新的 M;
3)M与P数量的关系:
M 与 P 的数量没有绝对关系,一个 M 阻塞,P 就会去创建或者切换另一个 M;
即使 P 的默认数量是 1,也有可能会创建很多个 M 出来;
创建
1)P 何时创建:
在确定了 P 的最大数量 n 后,运行时系统会根据这个数量创建 n 个 P。
2)M 何时创建:
没有足够的 M 来关联 P 并运行其中的可运行的 G时,会创建新的M;
比如所有的 M 此时都阻塞住了,而 P 中还有很多就绪任务,就会去寻找空闲的 M,而没有空闲的,就会去创建新的 M;
4.调度器的设计策略
线程复用
work stealing 机制
当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。
hand off 机制
当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。
利用并行
GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。
抢占
在 coroutine 中要等待一个协程主动让出 CPU 才执行下一个协程;
在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死;
全局 G 队列
在新的调度器中依然有全局 G 队列,但功能已经被弱化了;
当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G;
5.GPM调度流程
从上图我们可以分析出几个结论:
1)我们通过 go func () 来创建一个 goroutine;
2)有两种存储 G 的队列,一个是局部调度器 P 的本地队列、一个是全局 G 队列;新创建的 G 会先保存在 P 的本地队列中,如果 P 的本地队列已经满了就会保存在全局的队列中;
3)G 只能运行在 M 中,一个 M 必须持有一个 P,M 与 P 是 1:1 的关系;M 会从 P 的本地队列弹出一个 G 来执行,如果 P 的本地队列为空,就会想其他的 MP 组合偷取一个可执行的 G;
4) 一个 M 调度 G 执行的过程是一个循环机制;
5)当 M 执行某一个 G 时候如果发生了 syscall 或则其余阻塞操作,M 会阻塞;如果当前有一些 G 在执行,runtime 会把这个线程 M 从 P 中摘除 (detach);然后再创建一个新的操作系统的线程 (如果有空闲的线程可用就复用空闲线程) 来服务于这个 P;
6)当 M 系统调用结束时候,这个 G 会尝试获取一个空闲的 P 执行,并放入到这个 P 的本地队列;如果获取不到 P,那么这个线程 M 变成休眠状态, 加入到空闲线程中,然后这个 G 会被放入全局队列中。