GMP模型:
GMP是GO运行时调度层面的实现。有4个重要结构:G、M、P、Sched。对照上面的图理解
G:表示goroutine ,G的数量理论上无限制,只受内存的影响。创建一个G的初始内存为2-4k,低配计算机也能创建几十万个协程,而且执行完的 G,go语言还是会把他 P 本地/全局队列的闲置列表等待复用。
M:表示内核线程(machine),是对操作系统的一个封装,操作系统感知不到协程的存在,所以为了在cpu上执行代码必须要有这个内核态线程。
流程: M 是由操作系统创建,再绑定了有效的 P 后,进入一个调度循环获取 G(调度循环是 从 P 的本地/全局队列中获取 G,并执行 G 的函数,最后调用goexit做清理工作再回到 M,如此反复)。M 不会保留 G 的状态,这是 G 可以跨 M 调度的基础。 M 的数量有限制,默认是1w个,可以自行设置(debug.SetMaxThreads()),如果 M 有空闲就会回收或者休眠(休眠是为了后续线程的复用)。
P:表示调度器(processor),处理 M 执行 G 所需的上下文资源,只有将 G 和 M 绑定,才能让 P 的本地队列中的 G 真正运行起来。P 的数量决定了系统中最大可并行的 G 的数量。 P 的数量受计算机的CPU内核数影响(可以通过$GOMAXPROCS或在runtime.GOMAXPROCS()来设置,默认是计算机的cpu内核数,但是最大并不能超过这个数。该拯救者16核)
Sched: 调度器结构,用于维护 M 和 G 的全局队列,以及调度器的一些状态信息。
GOMAXPROCS是cpu的核心数,最多创建这么多 P (比如电脑是16核,最多就是16个 P )
在 GPM 模型,有一个全局队列(Global Queue):存放等待运行的 G,还有一个 P 的本地队列:也是存放等待运行的 G,但数量有限,不超过 256 个。GPM 的调度流程从 go func()开始创建一个 goroutine,新建的 goroutine 优先保存在 P 的本地队列中,如果 P 的本地队列已经满了,则会保存到全局队列中。M 会从 P 的队列中取一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会从其他的 MP 组合偷取一个(workStealing机制)可执行的 G 来执行,当 M 执行某一个 G 时候发生系统调用或者阻塞,M 阻塞,如果这个时候 G 在执行,runtime 会把这个线程 M 从 P 中摘除,然后创建一个新的操作系统线程(内核态线程 M)来服务于这个 P,当 M 系统调用结束时,这个 G 会尝试获取一个空闲的 P 来执行,并放入到这个 P 的本地队列,如果这个线程 M 变成休眠状态,加入到空闲线程中,然后整个 G 就会被放入到全局队列中。
关于 G,P,M 的个数问题,G 的个数理论上是无限制的,但是受内存限制,P 的数量一般建议是逻辑 CPU 数量的 2 倍,M 的数据默认启动的时候是 10000,内核很难支持这么多线程数,所以整个限制客户忽略,M 一般不做设置,设置好 P,M 一般都是要大于 P。
GM模型:
最开始的是GM模型没有 P 的,当然也是M:N的两级线程模型,但是会出现一些问题:
- 全局队列的锁竞争。M从全局队列中添加或获取 G 的时候,都是需要上锁的(下图执行步骤要加锁),这样就会导致锁竞争,虽然达到了并发安全,但是性能是非常差的。
- M 转移 G 会有额外开销。M 在执行 G 的时候,假设 M1 执行的 G1 创建了 G2,新创建的就要放到全局队列中去,但是这时有一个空闲的 M2 获取到了 G2,那么这样 G1、G2 会被不同的 M 执行,但是 M1 中本来就有 G2 的信息,M2 在 G1 上执行是更好的,而且取和放到全局队列也会来回加锁,这样都会有一部分开销。
- 线程的使用效率不能最大化。M 拿不到的时候就会一直空闲,阻塞的时候也不会切换。
也就是没有 workStealing 机制和 handOff 机制