场景1.G1创建G2
go func()
局部性:G2 优先加入到 P1 的本地队列
场景2.G1执行完毕
G1 运行完成后 (函数:goexit),M 上运行的 goroutine 切换为 G0,G0 负责调度时协程的切换(函数:schedule)。从 P 的本地队列取 G2,从 G0 切换到 G2,并开始运行 G2 (函数:execute)。实现了线程 M1 的复用。
场景3.G2开辟过多的G
场景4.G2本地满再创建G7
本地队列的前一半转移到全局队列(防止饥饿)
这些 G 被转移到全局队列时,会被打乱顺序。所以 G3,G4,G7 被转移到全局队列。
场景5.G2本地未满创建G8
G8 加入到 P1 点本地队列的原因还是因为 P1 此时在与 M1 绑定,而 G2 此时是 M1 在执行。所以 G2 创建的新的 G 会优先放置到自己的 M 绑定的 P 上。
场景6.唤醒正在休眠的M
在创建 G 时,运行的 G 会尝试唤醒其他空闲的 P 和 M 组合去执行。
假定 G2 唤醒了 M2,M2 绑定了 P2,并运行 G0,但 P2 本地队列没有 G,M2 此时为自旋线程(没有 G 但为运行状态的线程,不断寻找 G)。
场景7.被唤醒的M2从全局队列取批量G
自旋线程M2,优先从全局队列获取一批G放到P2的本地队列(函数:findrunnable()).M2从全局队列取的G数量符合下面的公式
n=min(len(GQ)/GOMAXPROCS+1,len(GQ/2))
场景8.M2从M1偷取G
取后半部分
场景9.自旋线程的最大限制
自旋线程+执行线程 ≤ GOMAXPROCS
超过的线程将加入休眠线程队列
场景10.G发生系统调用/阻塞
假定当前除了 M3 和 M4 为自旋线程,还有 M5 和 M6 为空闲的线程 (没有得到 P 的绑定,注意我们这里最多就只能够存在 4 个 P,所以 P 的数量应该永远是 M>=P, 大部分都是 M 在抢占需要运行的 P),G8 创建了 G9,G8 进行了阻塞的系统调用,M2 和 P2 立即解绑,P2 会执行以下判断:如果 P2 本地队列有 G、全局队列有 G 或有空闲的 M,P2 都会立马唤醒 1 个 M 和它绑定,否则 P2 则会加入到空闲 P 列表,等待 M 来获取可用的 p。本场景中,P2 本地队列有 G9,可以和其他空闲的线程 M5 绑定。
场景11.G发生系统调用/非阻塞
G8由阻塞变为非阻塞
M2 和 P2 会解绑,但 M2 会记住 P2,然后 G8 和 M2 进入系统调用状态。当 G8 和 M2 退出系统调用时,会尝试获取 P2,如果无法获取,则获取空闲的 P,如果依然没有,G8 会被记为可运行状态,并加入到全局队列,M2 因为没有 P 的绑定而变成休眠状态 (长时间休眠等待 GC 回收销毁)。