Go runtime 可以形象的理解为 Go 程序运行时的环境,类似于 JVM。不同于 JVM 的是,Go 的 runtime 与业务程序直接打包在一块,是一个可执行文件,直接运行在操作系统上,效率很高。
runtime 包含了一些 Go 的一些非常核心的功能:协程调度、垃圾回收、内存分配等。本文将着重介绍协程调度(GMP 模型)。
协程调度是指 Go 如何管理和执行协程,Go 的协程调度基于 GMP 模型。即:
GMP 三者的关系:
假如主机是单逻辑 CPU 的,那么 GMP 是这样的:
红色部分表示休眠或者挂起状态,黄色代表等待执行,绿色表示正在运行。系统初始化了两个线程,但我们只有一个处理器(P), M1 没有获取到 P,所以只能休眠。M0 当前获取到 P ,正在处理 G0, LRQ 里面目前有三个 G 在排队等待被 M 运行,GRQ 里面保存着 G4、G5、G6,表示它们还没有分配到队列中。
P 这个时候会分别对 LRQ 进行周期队列轮转 和 GRO 周期性检查:
假设 G0 遇到了系统调用:
等到 M1 中所有的协程执行完或者 M1 处理某个协程也遇到了了系统调用,就会重新释放 P 给其他空闲的 M。而另外一边 G0 的系统调用结束后,就会将 M0 线程从挂起状态变成休眠状态,并将 G0 放入 GRQ,等待被 P 重新调入 LRQ 中轮转执行。
如果我们的主机具备多个逻辑 CPU,创建了多个 P,那么就会变成多个线程并行执行:
多线程同时处理时,很有可能多个 LRQ 是不均衡的。假如上图的 M0 已经执行完了,其他线程还处于繁忙状态,M0 所绑定的 P 就会去检查 GQR,GQR 中也没有 G,那么它就会去偷取其他 LRQ 一部分的 G 来执行,一般每次会偷取一半。
runtime.GOMAXPROCS()
可以用来设置 P 的数量,一般设置为和逻辑 CPU 数量相等的值:
fmt.Println(runtime.NumCPU())
runtime.GOMAXPROCS(runtime.NumCPU()) // 使用所有的逻辑 CPU
// 结果
我的主机 CPU 是16核24线程,所以会使用24个 P
runtime.Gosched()
用于让出当前协程的运行时间片,也就是当 P 遇到它时,会先安排其他协程先执行:
func main() {
runtime.GOMAXPROCS(1)
go func() {
for i := 0; i < 5; i++ {
fmt.Println("go")
}
}()
runtime.Gosched()
fmt.Println("hello")
}
// 结果
输入结果不是固定的,有可能是
go
go
go
go
go
hello
也有可能是
go
go
go
go
hello
go
也有可能是
hello
输出第一种情况容易理解,主协程让出了时间片,理所应当先打印 Go,但是如果子协程还没有来得及被调度或者打印,就会出现其他情况。
runtime.Goexit() 会结束当前的协程,但是 defer 语句会正常执行。此语法不能在主函数中使用,会引发 panic:
func main() {
runtime.GOMAXPROCS(1)
go func() {
defer fmt.Println("defer不受影响")
fmt.Println("我被执行了")
runtime.Goexit()
fmt.Println("我被跳过了")
}()
time.Sleep(1 * time.Second)
}
// 结果
我被执行了
defer不受影响
本系列文章:
手机扫一扫
移动阅读更方便
你可能感兴趣的文章