本文是《Go语言调度器源代码情景分析》系列的第18篇,也是第四章《Goroutine被动调度》的第1小节。
前一章我们详细分析了调度器的调度策略,即调度器如何选取下一个进入运行的goroutine,但我们还不清楚什么时候以及什么情况下会发生调度,从这一章开始我们就来讨论这个问题。
总体说来,go语言的调度器会在以下三种情况下对goroutine进行调度:
- goroutine执行某个操作因条件不满足需要等待而发生的调度;
- goroutine主动调用Gosched()函数让出CPU而发生的调度;
- goroutine运行时间太长或长时间处于系统调用之中而被调度器剥夺运行权而发生的调度。
本章主要分析我们称之为被动调度的第1种调度,剩下的两种调度将在后面两章分别进行讨论。
Demo例子
我们以一个demo程序为例来分析因阻塞而发生的被动调度。
package main
func start(c chan int) {
c <- 100
}
func main() {
c := make(chan int)
go start(c)
<- c
}
该程序启动时,main goroutine首先会创建一个无缓存的channel,然后启动一个goroutine(为了方便讨论我们称它为g2)向channel发送数据,而main自己则去读取这个channel。
这两个goroutine读写channel时一定会发生一次阻塞,不是main goroutine读取channel时发生阻塞就是g2写入channel时发生阻塞。
创建g2 goroutine
首先用gdb反汇编一下main函数,看看汇编代码。
0x44f4d0<+0>: mov %fs:0xfffffffffffffff8,%rcx
0x44f4d9<+9>: cmp 0x10(%rcx),%rsp
0x44f4dd<+13>: jbe 0x44f549 <main.main+121>
0x44f4df<+15>: sub $0x28,%rsp
0x44f4e3<+19>: mov %rbp,0x20(%rsp)
0x44f4e8<+24>: lea 0x20(%rsp),%rbp
0x44f4ed<+29>: lea 0xb36c(%rip),%rax
0x44f4f4<+36>: mov %rax,(%rsp)
0x44f4f8<+40>: movq $0x0,0x8(%rsp)
0x44f501<+49>: callq 0x404330 <runtime.makechan> #创建channel
0x44f506<+54>: mov 0x10(%rsp),%rax
0x44f50b<+59>: mov %rax,0x18(%rsp)
0x44f510<+64>: movl $0x8,(%rsp)
0x44f517<+71>: lea 0x240f2(%rip),%rcx
0x44f51e<+78>: mov %rcx,0x8(%rsp)
0x44f523<+83>: callq 0x42c1b0 <runtime.newproc> #创建goroutine
0x44f528<+88>: mov 0x18(%rsp),%rax
0x44f52d<+93>: mov %rax,(%rsp)
0x44f531<+97>: movq $0x0,0x8(%rsp)
0x44f53a<+106>: callq 0x405080 <runtime.chanrecv1> #从channel读取数据
0x44f53f<+111>: mov 0x20(%rsp),%rbp
0x44f544<+116>: add $0x28,%rsp
0x44f548<+120>: retq
0x44f549<+121>: callq 0x447390 <runtime.morestack_noctxt>
0x44f54e<+126>: jmp 0x44f4d0 <main.main>
从main函数的汇编代码我们可以看到,创建goroutine的go关键字被编译器翻译成了对runtime.newproc函数的调用,第二章我们对这个函数的主要流程做过详细分析,这里简单的回顾一下:
- 切换到g0栈;
- 分配g结构体对象;
- 初始化g对应的栈信息,并把参数拷贝到新g的栈上;
- 设置好g的sched成员,该成员包括调度g时所必须pc, sp, bp等调度信息;
- 调用runqput函数把g放入运