Golang 协程调度之 GMP 模型

2,916 阅读2分钟

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

Go并发的真理: Do not communicate by sharing memory; instead, share memory by communicating. 不要以共享内存的方式来通信,相反,要通过通信来共享内存。

Go语言为并发而生,它可以轻松构造上万的协程。而协程的调度是通过 GMP 模型来实现的。

  • M,Machine,表示系统级线程,goroutine 是跑在 M 上的。线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列一批G放到P的本地队列,或从其他P的本地队列一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。
  • P,processor,是 goroutine 执行所必须的上下文环境,可以理解为协程处理器,是用来执行 goroutine 的。processor 维护着可运行的 goroutine 队列,里面存储着所有需要它来执行的 goroutine。
  • G,协程。

线程是运行 goroutine 的实体,调度器的功能是把可运行的 goroutine 分配到工作线程上

image.png

Go 调度本质是把大量的 goroutine 分配到少量线程上去执行,并利用多核并行,实现更强大的并发。

在某一时刻,M 和 P 是一对一的关系,P 和 G 是一对多的关系。在 M 的生命周期内,它只会与一个内核线程绑定,而 M 和 P 以及 P 和 G 之间的关系是动态可变的。

M 和 P 会适时的组合和断开,保证 P 中的待执行 G 队列能够得到及时运行。比如说上图中的 G0 此时因为网络 I/O 而阻塞了 M,那么 P 就会携带剩余的 G 投入到其他 M 的怀抱中。(在很多时候,P 的数量要比 M 多,)

image.png

当一个 P 在执行一个协程时,其他的会在等待。当正在执行的协程遇到阻塞情况,例如 IO 操作等,go 的处理器就会去执行其他的协程。

go的协程是非抢占式的,由协程主动交出控制权,也就是说,上面在发生IO操作时,并不是调度器强制切换执行其他的协程,而是当前协程交出了控制权,调度器才去执行其他协程。我们列举一下goroutine可能切换的点:

  • IO,select
  • channel
  • 等待锁
  • runtime.Gosched()

正因为是非抢占式的,所以才轻松的构造上万的协程,如果是抢占式,那么就会在切换任务时,保存当前的上下文环境,因为当前线程如果正在做一件事,做到一半,我们就强制停止,这时我们就必须多保存很多信息,避免再次切换回来时任务出错。