Goroutine调度器 | 青训营笔记

92 阅读2分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记

众所周知Go区别于其他编程语言最大的特点之一就是Go原生支持并发,那么Go是如何支持并发的呢?

Go使用Goroutine这一轻量级的用户级线程代替了操作系统的线程,因此它不依赖于操作系统的调度,Goroutine在进行上下文切换时开销更小。由于Goroutine是用户级线程,操作系统不可见,因此一个Go程序中的众多Goroutine就靠调度器来执行调度了。

Goroutine调度器m模型经历了从G-M到G-P-M两个阶段。
G-M模型:
G代表了一个Goroutine,M则代表了一个操作系统线程,G-M模型就是将G调度到M上运行。这个模型比较简单直接,但也存在问题:

  1. 由于全局互斥锁(Sched.Lock)的存在,对Goroutine的相关操作都需要上锁。
  2. Goroutine经常会在M之间传递,增加了额外的开销。
  3. 每个M都需要做内存缓存,增加了内存占用。
  4. 系统调用带来的频繁的工作线程的阻塞和解除阻塞,增加了性能开销。

G-P-M模型:
G: 代表Goroutine,存储了Goroutine的执行栈信息、Goroutine状态以及Goroutine的任务函数等,而且G对象是可以重用的;
P: 代表逻辑processor,P的数量决定了系统内最大可并行的G的数量,P的最大作用还是其拥有的各种G对象队列、链表、一些缓存和状态;
M: 代表着真正的执行计算资源。在绑定P后,进入一个调度循环,而调度循环的机制是从P的本地运行队列以及全局队列中获取G,切换到G的执行栈上并执行G的函数,调用goexit做清理工作并回到M,如此反复。M并不保留G状态,这是 G可以跨M调度的基础。

G的调度: Go程序启动时,运行时会去启动一个名为sysmon的监控线程,如果一个G运行10ms,sysmon就会发出抢占式调度的请求。等到这个G下一次调用函数或方法时,运行时就可以将G抢占并移出运行状态,放入队列中,等待下一次被调度。

image.png 图片来自极客时间《Tony Bai·Go语言第一课》