go GMP模型

Posted by Liao on 2022-04-29

一、GM模型

go1.1版本之前是用GM模型的:

缺点:

  • 全局队列的锁竞争问题。M从全局队列获取G时,都要获取队列的锁,导致激烈的锁竞争,性能下降
  • M的使用效率不能最大化,增加空转时间(没有work-stealing 和 hand-off机制)
  • M转移G增加额外开销。例如M1在运行G1时,创建了G2。为了继续执行G1,需要把G2交给其它的M处理,或者放在全局队列中,不能保证是由M1处理,造成很差的局部性

二、GMP模型

2.1 GMP模型

GMP是go调度器的原理

有四个重要的结构,G、M、P、sched

G(Goroutine)

存储了Goroutine的执行栈信息、Goroutine状态、Goroutine的任务函数等。

P(Processor)

  • 将可运行的goroutine分配到工作线程上。如果p队列goroutine为空,则会尝试从全局队列或者别的本地队列偷G过来。
  • P的数量默认是本机CPU核数,但可以通过环境变量$GOMAXPROCS 或者 rumtime.GOMAXPROCS()来设置

M(Machine)

  • Go对OS thread 的封装,可以看作内核级线程,是运行Goroutine的实体。
  • 在CUP上运行代码必须要有线程,通过clone()创建。
  • M绑定有效的P之后,会进入一个调度循环,即从P的本地队列及全局队列中获取G。M数量默认限制为10000,可以通过debug.SetMaxThreads()方法进行设置。
  • 一个 M 阻塞了,会创建新的 M。

Sched:调度器结构

操作系统把内核级线程调度到cpu上执行

2.2 GMP调度流程

  1. go func创建协程goroutine,放在局部队列中,若满则放在全局队列
  2. M相当于是消费者,首先从本地队列获取G,如果取不到则取全局队列获取,再取不到则从别的P中“偷取”一半G (work stealing机制)
  3. M拿到G之后执行调度;否则会被阻塞,M会处于自旋的状态(不断获取G)
    1. M执行G过程中如果发生系统调用systemCall() 而阻塞,会阻塞M和G,则会释放P,让另一个M执行(hand off机制)。如果没有空闲的M,则会新建一个M,接管当前正在阻塞G所属的P,接着执行P中剩余的G; 系统调用结束后,M会尝试获取空闲的P(优先获取之前绑定的P),若获取不到空闲P,则这个空闲的M会休眠,加入到休眠队列中,
    2. M执行G过程中如果发生网络IO操作而阻塞(异步),则会阻塞G,但不会阻塞M。M继续寻找P中其它可执行的G继续执行,而被阻塞的G会被网络轮询器(network poller)接手。当G被恢复之后,把这个G放回本地队列中,重新进入可执行状态。
  4. M执行完G之后会清理现场,重新进入调度循环

2.3 Goroutine的调度机制

2.3.1 work strealing机制

​ M首先从本地队列获取G,若p为空,则去全局队列找G;若全局队列也为空,则从另一个本地队列偷取一半数量的G,以防止M空闲。若M空闲,只能自旋。

2.3.2 hand off机制

​ M执行G过程中如果发生系统调用systemCall()而阻塞,会阻塞M和G,P和M解绑,改P让另一个M执行。

2.4 Goroutine的调度时机

  • 抢占式调度
    • sysmon检测到goroutine执行过久(如sleep,死循环)
  • 主动调度
    • go关键字 go func() 新起一个协程,触发调度循环
    • 主动调用runtime.GoSched()
    • 垃圾回收,stop the world之后,会重新选择G开始执行
  • 被动调度
    • 系统调用阻塞(异步)
    • 网络IO调用阻塞(异步)
    • atomic/mutext/channel等阻塞(异步),阻塞G,G移动到channel的等待队列中,M执行P剩余的G

参考

https://learnku.com/articles/41728