2.5 说说你对 Go 里的抢占式调度的理解 ==================================== Go 从 v1.1 发现展到目前的 v1.16,协程调度策略也在不断的完善优化。 下面我将从 v.1.1 开始讲讲 协程调度策略中抢占式调度的发展历程。 v1.1 的非抢占式调用 ------------------- 在最初的 v1.1 版本中,只有当一个协程主动让出 CPU 资源(可以是运行结束,也可以是发生了系统调用或其他阻塞性操作),才能触发调度,进行下一个协程。 而如果一个协程运行了很久,也没有主动让出的动作发生,就会自私的一个人占用整个线程,该线程无法再去运行其他的 goroutine 了。 这种策略会让 Go 的并发性大打折扣,名不符实。 v1.2 基于协作的抢占式调用 ------------------------- 由于 v1.1 的非抢占式调用,以程序的并发效率影响实在太大。因为在下一个版本 v1.2 就紧急地对调度策略进行了临时的优化。经过优化后,go 从 v1.2 开始支持抢占式的调用: 1. 如果 sysmon 监控线程发现有个协程 A 执行之间太长了(或者 gc 场景,或者 stw 场景),那么会友好的在这个 A 协程的某个字段设置一个抢占标记 ; 2. 协程 A 在 call 一个函数的时候,会复用到扩容栈(morestack)的部分逻辑,检查到抢占标记之后,让出 cpu,切到调度主协程里; 之所以说 v1.2 的抢占式调用是临时的优化方案,是因为这种抢占式调度是基于协作的。在一些的边缘场景下,协程还是在会独自占用整个线程无法让出。 从上面的流程中,你应该可以注意到,A 调度权被抢占有个前提:A 必须主动 call 函数,这样才能有走到 morestack 的机会。 反面案例可以看下面这个程序,当运行到 ``time.Sleep`` 后,线程上的 goroutine 会从 main 切换到前面的匿名函数协程,而这个匿名函数协程并是在作for 死循环,并没有任何可以让出 cpu 运行权的操作,因为该程序在 go 1.14 之前的 go版本中,运行后会一直卡住,而不会打印 ``I got scheduled!`` .. code:: go package main import ( "fmt" "runtime" "time" ) func main() { runtime.GOMAXPROCS(1) fmt.Println("The program starts ...") go func() { for { } }() time.Sleep(time.Second) fmt.Println("I got scheduled!") } v1.14 基于信号的抢占式调用 -------------------------- 基于协作的抢占式调用,伴随着 Go 走过了12个版本,终于在 v1.14 迎来了真正的抢占式调用。 为什么说是真正的抢占式调用呢? 因为 v1.14 的这种抢占式调用是基于信号的,不管你的协程有没有意愿主动让出 cpu 运行权,只要你这个协程超过某个时间,就会发送信号强行夺取 cpu 运行权。 那么这个时间具体是多少呢? 20ms 延伸阅读 -------- - `go trace 剖析 go1.14 异步抢占式调度 `__ - `Go语言设计与实现 - 抢占式调度器 `__