原文地址:Goroutine调度器-主动调度
goroutine主动调度指的是当前正在运行的goroutine通过直接调用runtime.Gosched()暂时放弃运行而发生的调度。
主动调度完全是用户代码控制的,来看下面这个例子:
package main
import (
"runtime"
"sync"
)
const N = 1
func main() {
var wg sync.WaitGroup
wg.Add(N)
for i := 0; i < N; i++ {
go start(&wg)
}
wg.Wait()
}
func start(wg *sync.WaitGroup) {
for i := 0; i < 1000 * 1000 * 1000; i++ {
runtime.Gosched()
}
wg.Done()
}
上述代码在main goroutine创建了一个新的称为g2的goroutine去执行start,g2在start循环中反复调用Gosched放弃执行权,主动将CPU让给调度器调度。
来看runtime/proc.go文件262行代码分析主动调度的入口函数Gosched:
// Gosched yields the processor, allowing other goroutines to run. It does not
// suspend the current goroutine, so execution resumes automatically.
func Gosched() {
checkTimeouts() //amd64 linux平台空函数
//切换到当前m的g0栈执行gosched_m函数
mcall(gosched_m)
//再次被调度起来则从这里开始继续运行
}
因为主要看g2状态,所以用b proc.go: 266在Gosched的mcall(gosched_m)这行打个断点,之后运行程序等停止,反汇编当前正在执行的函数,得到汇编代码如下:
(gdb) disass
Dump of assembler code for function main.start:
0x000000000044fc90 <+0>:mov %fs:0xfffffffffffffff8,%rcx
0x000000000044fc99 <+9>:cmp 0x10(%rcx),%rsp
0x000000000044fc9d <+13>:jbe 0x44fcfa <main.start+106>
0x000000000044fc9f <+15>:sub $0x20,%rsp
0x000000000044fca3 <+19>:mov %rbp,0x18(%rsp)
0x000000000044fca8 <+24>:lea 0x18(%rsp),%rbp
0x000000000044fcad <+29>:xor %eax,%eax
0x000000000044fcaf <+31>:jmp 0x44fcd0 <main.start+64>
0x000000000044fcb1 <+33>:mov %rax,0x10(%rsp)
0x000000000044fcb6 <+38>:nop
0x000000000044fcb7 <+39>:nop
=> 0x000000000044fcb8 <+40>:lea 0x241e1(%rip),%rax # 0x473ea0
0x000000000044fcbf <+47>:mov %rax,(%rsp)
0x000000000044fcc3 <+51>:callq 0x447380 <runtime.mcall>
0x000000000044fcc8 <+56>:mov 0x10(%rsp),%rax
0x000000000044fccd <+61>:inc %rax
0x000000000044fcd0 <+64>:cmp $0x3b9aca00,%rax
0x000000000044fcd6 <+70>:jl 0x44fcb1 <main.start+33>
0x000000000044fcd8 <+72>:nop
0x000000000044fcd9 <+73>:mov 0x28(%rsp),%rax
0x000000000044fcde <+78>:mov %rax,(%rsp)
0x000000000044fce2 <+82>:movq $0xffffffffffffffff,0x8(%rsp)
0x000000000044fceb <+91>:callq 0x44f8f0 <sync.(*WaitGroup).Add>
0x000000000044fcf0 <+96>:mov 0x18(%rsp),%rbp
0x000000000044fcf5 <+101>:add $0x20,%rsp
0x000000000044fcf9 <+105>:retq
0x000000000044fcfa <+106>:callq 0x447550 <runtime.morestack_noctxt>
0x000000000044fcff <+111>:jmp 0x44fc90 <main.start>
当前正在执行main.start而不是runtime.Gosched,在整个start中都没有Gosched,它被编译器优化掉了,程序停0x000000000044fcb8 <+40>: lea 0x241e1(%rip),%rax处,该指令下第二条callq调用runtime.mcall,接下来使用si 2执行这两条指令,之后使用i r rsp rbp rip记录CPU的rsp、rbp、rip的值,如下:
(gdb) i r rsp rbp rip
rsp 0xc000031fb0 0xc000031fb0
rbp 0xc000031fc8 0xc000031fc8
rip 0x44fcc3 0x44fcc3 <main.start+51>
0x000000000044fcc3处的callq先将其后首条指令地址0x000000000044fcc8放入g2的栈,之后跳到mcall首条指令处开始执行。
mcall将完成如下工作:
-
将上面call压栈的返回地址0x000000000044fcc8取出并保存在g2的sched.pc,将上面查到的rsp(0xc000031fb0)和rbp(0xc000031fc8)的值分别保存在sched.sp和sched.bp,代表g2的调度现场信息。
-
将保存在g0的sched.sp和sched.bp的值分别恢复到CPU的rsp和rbp,完成从g2的栈到g0的栈的切换。
-
在g0栈执行gosched_m(runtime.Gosched调用mcall传给mcall的参数)。
来看runtime/proc.go文件2623行代码分析gosched_m:
// Gosched continuation on g0.
func gosched_m(gp *g) {
if trace.enabled { //traceback 不关注
traceGoSched()
}
goschedImpl(gp) //我们这个场景:gp = g2
}
gosched_m只是简单调用goschedImpl,来看runtime/proc.go文件2608行:
func goschedImpl(gp *g) {
......
casgstatus(gp, _Grunning, _Grunnable)
dropg() //设置当前m.curg = nil, gp.m = nil
lock(&sched.lock)
globrunqput(gp) //把gp放入sched的全局运行队列runq
unlock(&sched.lock)
schedule() //进入新一轮调度
}
goschedImpl有一g指针类型的形参,此场景传给它的实参是g2,goschedImpl先将g2状态从_Grunning设为_Grunnable,并通过dropg解除当前工作线程m与g2间的关联(m.curg设为nil,g2.m设为nil),之后调globrunqput将g2放入全局运行队列。
g2放入全局运行队列后,g2及其它部分状态如下:
可以看到,g2被放入sched的全局运行队列,该队列有个head头指针指向队列中首个g对象,还有tail尾指针指向队列最后一个g对象,g2的sched保存了调度所需全部现场信息,当g2再次被schedule调度时,gogo将信息恢复到CPU的rsp、rbp、rip中,使g2又可从0x44fcc8处开始在g2栈中执行g2代码。
将g2放入sched的全局运行队列后,goschedImpl继续调schedule进入下轮调度循环。
至此,g2通过自主调Gosched,实现主动调度。
以上仅为个人观点,不一定准确,能帮到各位那是最好的。
好啦,到这里本文就结束了,喜欢的话就来个三连击吧。
扫码关注公众号,获取更多优质内容。