这是我参与「第三届青训营 -后端场」笔记创作活动的的第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上运行。这个模型比较简单直接,但也存在问题:
- 由于全局互斥锁(Sched.Lock)的存在,对Goroutine的相关操作都需要上锁。
- Goroutine经常会在M之间传递,增加了额外的开销。
- 每个M都需要做内存缓存,增加了内存占用。
- 系统调用带来的频繁的工作线程的阻塞和解除阻塞,增加了性能开销。
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抢占并移出运行状态,放入队列中,等待下一次被调度。
图片来自极客时间《Tony Bai·Go语言第一课》