✍个人博客:Pandaconda-CSDN博客
📣专栏地址:http://t.csdnimg.cn/UWz06
📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪
154. Go 调度器的调度时机
什么时候进行调度(执行/切换)?
在以下情形下,会切换正在执行的 goroutine。
- 抢占式调度
- sysmon 检测到协程运行过久(比如 sleep,死循环)
- 切换到 g0,进入调度循环
- sysmon 检测到协程运行过久(比如 sleep,死循环)
- 主动调度
- 新起一个协程和协程执行完毕
- 触发调度循环
- 主动调用 runtime.Gosched()
- 切换到 g0,进入调度循环
- 垃圾回收之后
- stw 之后,会重新选择 g 开始执行
- 新起一个协程和协程执行完毕
- 被动调度
- 系统调用(比如文件 IO)阻塞(同步)
- 阻塞 G 和 M,P 与 M 分离,将 P 交给其它 M 绑定,其它 M 执行 P 的剩余 G
- 网络 IO 调用阻塞(异步)
- 阻塞 G,G 移动到 NetPoller,M 执行 P 的剩余 G
- atomic/mutex/channel 等阻塞(异步)
- 阻塞 G,G 移动到 channel 的等待队列中,M 执行 P 的剩余 G
- 系统调用(比如文件 IO)阻塞(同步)
155. Go 调度器的调度策略
使用什么策略来挑选下一个 goroutine 执行?
由于 P 中的 G 分布在 runnext、本地队列、全局队列、网络轮询器中,则需要挨个判断是否有可执行的 G,大体逻辑如下:
- 每执行 61 次调度循环,从全局队列获取 G,若有则直接返回
- 从 P 上的 runnext 看一下是否有 G,若有则直接返回
- 从 P 上的本地队列看一下是否有 G,若有则直接返回
- 上面都没查找到时,则去全局队列、网络轮询器查找或者从其他 P 中窃取,一直阻塞直到获取到一个可用的 G 为止
156. 调度策略如何实现?
func schedule() {
_g_ := getg()
var gp *g
var inheritTime bool
...
if gp == nil {
// 每执行61次调度循环会看一下全局队列。为了保证公平,避免全局队列一直无法得到执行的情况,当全局运行队列中有待执行的G时,通过schedtick保证有一定几率会从全局的运行队列中查找对应的Goroutine;
if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp = globrunqget(_g_.m.p.ptr(), 1)
unlock(&sched.lock)
}
}
if gp == nil {
// 先尝试从P的runnext和本地队列查找G
gp, inheritTime = runqget(_g_.m.p.ptr())
}
if gp == nil {
// 仍找不到,去全局队列中查找。还找不到,要去网络轮询器中查找是否有G等待运行;仍找不到,则尝试从其他P中窃取G来执行。
gp, inheritTime = findrunnable() // blocks until work is available
// 这个函数是阻塞的,执行到这里一定会获取到一个可执行的G
}
...
// 调用execute,继续调度循环
execute(gp, inheritTime)
}
从全局队列查找时,如果要所有 P 平分全局队列中的 G,每个 P 要分得多少个,这里假设会分得 n 个。然后把这 n 个 G,转移到当前 G 所在 P 的本地队列中去。但是最多不能超过 P 本地队列长度的一半(即 128)。这样做的目的是,如果下次调度循环到来的时候,就不必去加锁到全局队列中再获取一次 G 了,性能得到了很好的保障。
func globrunqget(_p_ *p, max int32) *g {
...
// gomaxprocs = p的数量
// sched.runqsize是全局队列长度
// 这里n = 全局队列的G平分到每个P本地队列上的数量 + 1
n := sched.runqsize/gomaxprocs + 1
if n > sched.runqsize {
n = sched.runqsize
}
if max > 0 && n > max {
n = max
}
// 平分后的数量n不能超过本地队列长度的一半,也就是128
if n > int32(len(_p_.runq))/2 {
n = int32(len(_p_.runq)) / 2
}
// 执行将G从全局队列中取n个分到当前P本地队列的操作
sched.runqsize -= n
gp := sched.runq.pop()
n--
for ; n > 0; n-- {
gp1 := sched.runq.pop()
runqput(_p_, gp1, false)
}
return gp
}
从其它 P 查找时,会偷一半的 G 过来放到当前 P 的本地队列。