先看一段代码:
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(1)
go func() {
for {
}
}()
time.Sleep(time.Millisecond)
fmt.Println("finished.")
}
这段代码在go1.13中运行是死循环,在go1.17中运行输出finished然后退出。
go的调度有以下几种方式:
1.主动调度
自己主动放弃执行,通过runtime.Gosched()实现。取消G与M之间的绑定关系,将G放入到全局运行对列中去。
2.被动调度
这个主要发生在sleep,channal堵塞,垃圾回收等情况。通过调用gopark函数完成别动调度。该函数会解除G与M之间的关系,然后根据被动调度的原因不同,执行不同的waitunlockf函数,并开始新一轮的执行。
3.抢占调度
主要抢占执行时间过长的G和处于系统调用阶段的G。
抢占调度依赖一个监控线程,该线程在独立的M上运行,不需要绑定逻辑处理器P。
1)抢占执行时间过长的G,发生在函数调用的时候。代码编译的时候,编译器会在函数调用前加入一些检测代码。当该G执行时间超过10ms时,监控线程会对这段检测代码中的某些标志位做设置,当发生函数调用时,检测到这些设置,发生重新调度。
2)抢占系统调用。当G在系统调用中超过10ms时,且任务比较繁忙时,将发生抢占调度(任务不忙时,抢占没有意义,任务繁忙的判断有一些具体的标准,该处省略)。抢占原理是将被系统调用线程M占用的P让出。该P让出后,可以与其他M绑定执行别的任务。
在1.13版本及之前大概就这么多。1.14开始有变化。
主要变化发生在 抢占调度 中存在问题。当cpu密集型任务执行时,可能没有发生函数调用,也没有系统调用,这样就没法进行抢占。 优化的目的就是要想办法打断G的执行,然后执行调度。这里采取的是通过信号来进行中断的方式。
监控线程检测到某个G执行时间过长,就发送信号_SIGURG,然后程序在处理信号时,修改了原程序中某些寄存器的值,使得程序继续执行的方向发生了改变。也就是当信号中断处理结束后,程序恢复运行的时候,不会再继续执行了,而是去执行新一轮的调度函数。
文章来源于互联网:go协程的调度方式