Go GPM 模型
线程与goroutine
goroutine属于用户态线程;传统线程是属于内核态线程,由操作系统调度。
下面主要从内存占用,上下文切换两个角度描述:
线程
线程是操作系统调度的基本单位,受操作系统调度器调度做线程的上下文切换。
进程控制块PCB,或者说线程控制块TCB 是内核的数据结构,控制着线程运行和调度的信息。记录如线程ID、线程状态、寄存器信息、程序计数器、调度优先级、用户栈和内核栈信息。
-
上下文切换
陷入内核,依赖众多寄存器保留现场,还原现场
-
内存占用
一个线程的内存占用(线程共享进程的地址空间),主要包括用户栈和内核栈的大小分配。
- 操作系统调参: ulimit -s
- 虚拟机如 Java虚拟机 JVM对线程栈内存控制参数:-Xss=512K
goroutine
goroutine 是协程的思路,可以理解是用户态线程。
-
上下文切换
不需要陷入内核,应用层面做调度,上下文切换更加轻量。当然执行时,还是要依赖操作系统的线程。
-
内存占用
一个 goroutine 的栈内存占用是 2 KB(不够用自动扩容),相比线程占用少的多。因此同样配置的机器,比如线程开几千个可能就榨干机器性能了,goroutine可能开十万个。
GPM 调度模型
GPM 各字母初步认识
GPM | 含义 |
---|---|
G | goroutine,用户态调度和执行单位 |
P | processer 处理器,包含了运行goroutine的资源(上下文环境),1对1 关联一个M |
M | machine 操作系统线程,goroutine执行还是要依赖线程 |
GPM 是如何协调运行的
简述:一个 G 的执行需要 P 和 M 的支持。一个 M 与一个 P 关联之后,形成一个有效的 G 的运行环境。每个 P 都包含一个可运行的 G 的队列。该队列中的 G 会被依次传给与本地 P 关联的 M,并获得运行时机。
两个队列
这里先介绍下两个 存放待运行 goroutine 的队列,一个是调度器下的全局队列,一个绑定在每个 Processer 的本地队列。
- P 的本地队列:存放的是等待运行的goroutine,当前运行的goroutine新建一个 gr 时,会优先放到对应这个P的本地队列。
- 全局队列:同样存放的是等待运行的goroutine,如上当本地队列满了,会移出部分 gr 到全局队列。如果某个P 对应的队列空了,会从全局队列取 gr 运行。
GPM 调度运行
G(goroutine) 的运行还是要依赖 线程(M)的,那它是用哪个线程去执行呢?这就需要调度器了,它把 线程M 和 处理器 P (processer) 做绑定,P 上有一个正在运行的goroutine 和 本地队列,当这个 gr 运行完,再从本地队列取下一个 gr,这么运行下去。
M 与 P 的创建
- M
一个 M 代表了一个内核线程。新创建一个 M 时,一般是由于没有足够的 M 来关联 P (当前 M 阻塞时,会在 空闲 M列表中找一个与 P 关联,如果没有则会新建一个 M)。
- P
P 的数量受 runtime.GOMAXPROCS 控制,默认是cpu核数,最大是256(未来可变)。
调度策略
- work stealing
从执行线程角度来看,当本线程 M 无可运行的 G 时,会尝试从全局队列中获取,没有则还会尝试从其他线程绑定的 P steal G,而不是销毁线程。
- hand off
当 gr 进行系统调用而阻塞时,运行时系统会把这个 gr 所依赖的 M 和与之关联的 P 进行分离。如果这个P的本地队列还有 gr,那么运行时系统会找一个空闲的 M 或者再创建一个 M 并与这个 P 关联。
参考
- 《Go 并发编程实战》