抢占调度
go 1.14 版本带来了一个非常重要的特性:异步抢占的调度模式。之前我们通过解释一个简单的协程调度原理(),并且实现协程调度例子都提到了一个点:协程是用户态实现的自我调度单元,每个协程都是君子才能维护和谐的调度秩序,如果出现了流氓(占着 cpu 不放的协程)你是无可奈何的。
go1.14 之前的版本所谓的抢占调度是怎么样的:
- 如果 sysmon 监控线程发现有个协程 A 执行之间太长了(或者 gc 场景,或者 stw 场景),那么会友好在这个 A 协程的某个字段设置一个抢占标记 ;
- 协程 A 在 call 一个函数的时候,会复用到扩容栈(morestack)的部分逻辑,检查到抢占标记之后,让出 cpu,切到调度主协程里;
这样 A 就算是被抢占了。我们注意到,A 调度权被抢占有个前提:A 必须主动 call 函数,这样才能有走到 morestack 的机会(旁白:能抢君子调度权利,抢占不了流氓的)。
举个栗子,然后看下 go1.13 和 go1.14 的分析对比:
特殊处理:
- 为了研究方便,我们只用一个 P(处理器),这样就确保是单处理器的场景;
- 回忆下 golang 的 GMP 模型:调度单元 G,线程 M,队列 P,由于 P 只有一个,所以每时每刻有效执行的 M 只会有一个,也就是单处理器的场景(旁白:个人理解有所不同,有些人喜欢直接把 P 理解成处理器,我这里把 P 说成队列是从实现的角度来讲的);
- 打开 golang 调试大杀器 trace 工具(可以直观的观察调度的情况);
- 搞一个纯计算且耗时的函数
calcSum
(不给任何机会);
下面创建一个名为 example.go
的文件,写入以下内容:
package main
import (
"fmt"
"os"
"runtime"
"runtime/trace"
"sync"
)
func calcSum(w *sync.WaitGroup, idx int) {
defer w.Done()
var sum, n int64
for ; n < 1000000000; n++ {
sum += n
}
fmt.Println(idx, sum)
}