goroutine切换
goroutine在go代码中无处不在,go程序会根据不同的情况去调度不同的goroutine,一个goroutine在某个时刻要么在运行,要么在等待,或者死亡。
goroutine的切换一般会在以下几种情况下发生:
- 基于信号抢占式的调度,一个goroutine如果运行很长,会被踢掉
- 发生系统调用,系统调用会陷入内核,开销不小,暂时解除当前goroutine
- channel阻塞,当从channel读不到或者写不进的时候,会切换goroutine
关于go的调度可以阅读golang 如何调度你的程序的
管理员-g0
go程序中,每个M都会绑定一个叫g0的初代goroutine,它在M的创建的时候创建,g0的主要工作就是goroutine的调度、垃圾回收等。g0和我们常规的goroutine的任务不同,g0的栈是在主线程栈上分配的,并且它的栈空间有64k,m0是runtime创建第一个线程,然后m0关联一个本地的p,就可以运行g0了。在g0的栈上不断的调度goroutine来执行,当有新的goroutine关联p准备运行发现没有m的时候,就会去创建一个m,m再关联一个g0,g0再去调度...
goroutine的创建
通过go tool compile -S main.go 我们来看看发生了什么?
汇编过于太长,只截取其中一部分。
我们看到有一行CALL runtime.newProc()的函数被调用了,这是通过起关键字go func创建goroutine的入口
通过gp:=getg()
来获取g0,然后通过systemstack
切到g0栈,再执行newproc1
,newproc1就是我们的goroutine诞生的地方。我们来看看newproc1干了什么:
- 如果我们的func为nil,则报错
- 如果我们的func的参数太多,则报错
- 获取本地的p
- 尝试从本地的p的gfree上获取一个不用的g,或者从全局的p中获取
- 没有获取到空闲的g的时候,则去创建一个g,默认大小为2k
- 新创建的g的状态gdead,防止gc错扫面
- 将新的g加入全局的allg列表中
- 初始化这个g的一些参数
- 将我们的func和这个g绑定
- 初始化完成后,将这个g的状态设置为runable,处于可以被执行状态
- 通过runqput将g放入p的队列,p的队列满的话,就放入全局队列
- 尝试通过wakep唤醒一个正处于休眠的p来执行
至此一个新的goroutine创建完毕。
gopark(goroutine的休眠)
goroutine的切换涉及到一个很重要的函数gopark。
gopark的作用:
- 将running状态的goroutine设置为waiting
- 解除goroutine和当前工作线程M的关系
- 获取一个新goroutine来运行
gopark函数的关键就是mcall函数调用的park_m。
park_m:
- gopark通过mcall将当前线程的堆栈切换到g0的堆栈
- 保存当前goroutine的上下文(pc、sp寄存器->g.sched)
- 在g0栈上,调用park_m
- 将当前的g从running状态设置成waiting状态
- 通过
dropg
来解除m和g的关系
func dropg() {
_g_ := getg()
setMNoWB(&_g_.m.curg.m, nil)
setGNoWB(&_g_.m.curg, nil)
}
复制代码
- 最后通过schedule来发起新一轮的调度
schedule()->execute()->gogo()
,gogo尝试从gobuf中恢复出协程执行状态并跳转到上一次指令处继续执行。
goready (goroutine的唤醒)
与gopark相反的,有一个goready的函数,它的作用就是唤醒waiting状态的goroutine
还是通过systemstack切到g0栈,在g0栈上发起调度
- 获取goroutine的状态
- 将waiting状态的goroutine切换到runable状态
- 尝试唤起一个p来执行当前goroutine
有疑问加站长微信联系(非本文作者)