GPM 是 Go 语言运行时(runtime)层面的实现,是 go 语言自己实现的一套调度系统。区别于操 作系统调度 OS 线程。
G
Goroutine,里面除了存放本 goroutine 信息外还有与所在 P 的绑定等信息。
P
Process,管理着一组 goroutine 队列,P里面会存储当前 goroutine 运行的上下文环境(函数 指针,堆栈地址及地址边界),P会对自己管理的 goroutine 队列做一些调度(比如把占用 CPU 时间较长的 goroutine 暂停、运行后续的 goroutine 等等)当自己的队列消费完了就去全 局队列里取,如果全局队列里也消费完了会去其他 P 的队列里抢任务。
M
Machine,是 Go 运行时(runtime)对操作系统内核线程的虚拟,M与内核线程一般是一一映 射的关系,一个 groutine 最终是要放到 M 上执行的。
调度
P 与 M 一般也是一一对应的。他们关系是:P管理着一组 G,挂载在 M 上运行。当一个 G 长久阻塞 在一个 M 上时,runtime 会新建一个 M,阻塞 G 所在的 P 会把其他的 G 挂载在新建的 M 上。当旧的 G 阻塞完成或者认为其已经死掉时,回收旧的 M。
P 的个数是通过 runtime.GOMAXPROCS
设定(最大 256),Go1.5 版本之后默认为物理线程数。
在并发量大的时候会增加一些 P 和 M,但不会太多,切换太频繁的话得不偿失。
单从线程调度讲,Go 语言相比起其他语言的优势在于 OS 线程是由 OS 内核来调度的,
goroutine 则是由 Go 运行时(runtime)自己的调度器调度的,这个调度器使用一个称为 m:n
调度的技术(复用/调度 m 个 goroutine 到 n 个 OS 线程)。其一大特点是 goroutine 的调度是在
用户态下完成的,不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在
用户态维护着一块大的内存池,不直接调用系统的 malloc
函数(除非内存池需要改变),
成本比调度 OS 线程低很多。另一方面充分利用了多核的硬件资源,近似的把若干 goroutine
均分在物理线程上,再加上本身 goroutine 的超轻量,以上种种保证了 go 调度方面的性能。