go高级进阶:goroutine的创建、休眠与恢复

2,122 阅读3分钟

goroutine切换

goroutine在go代码中无处不在,go程序会根据不同的情况去调度不同的goroutine,一个goroutine在某个时刻要么在运行,要么在等待,或者死亡。
goroutine的切换一般会在以下几种情况下发生:

  1. 基于信号抢占式的调度,一个goroutine如果运行很长,会被踢掉
  2. 发生系统调用,系统调用会陷入内核,开销不小,暂时解除当前goroutine
  3. 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再去调度...

image.png

goroutine的创建

carbon (14).png 通过go tool compile -S main.go 我们来看看发生了什么?

carbon (15).png 汇编过于太长,只截取其中一部分。
我们看到有一行CALL runtime.newProc()的函数被调用了,这是通过起关键字go func创建goroutine的入口

carbon (16).png 通过gp:=getg()来获取g0,然后通过systemstack切到g0栈,再执行newproc1,newproc1就是我们的goroutine诞生的地方。我们来看看newproc1干了什么:

carbon (18).png

  1. 如果我们的func为nil,则报错
  2. 如果我们的func的参数太多,则报错
  3. 获取本地的p
  4. 尝试从本地的p的gfree上获取一个不用的g,或者从全局的p中获取
  5. 没有获取到空闲的g的时候,则去创建一个g,默认大小为2k
  6. 新创建的g的状态gdead,防止gc错扫面
  7. 将新的g加入全局的allg列表中
  8. 初始化这个g的一些参数
  9. 将我们的func和这个g绑定
  10. 初始化完成后,将这个g的状态设置为runable,处于可以被执行状态
  11. 通过runqput将g放入p的队列,p的队列满的话,就放入全局队列
  12. 尝试通过wakep唤醒一个正处于休眠的p来执行 至此一个新的goroutine创建完毕。

gopark(goroutine的休眠)

goroutine的切换涉及到一个很重要的函数gopark。

carbon (13).png gopark的作用:

  1. 将running状态的goroutine设置为waiting
  2. 解除goroutine和当前工作线程M的关系
  3. 获取一个新goroutine来运行 gopark函数的关键就是mcall函数调用的park_m。

carbon (19).png park_m:

  1. gopark通过mcall将当前线程的堆栈切换到g0的堆栈
  2. 保存当前goroutine的上下文(pc、sp寄存器->g.sched)
  3. 在g0栈上,调用park_m
  4. 将当前的g从running状态设置成waiting状态
  5. 通过dropg来解除m和g的关系
func dropg() {
    _g_ := getg()
    setMNoWB(&_g_.m.curg.m, nil)
    setGNoWB(&_g_.m.curg, nil)
}
  1. 最后通过schedule来发起新一轮的调度 schedule()->execute()->gogo(),gogo尝试从gobuf中恢复出协程执行状态并跳转到上一次指令处继续执行。

goready (goroutine的唤醒)

与gopark相反的,有一个goready的函数,它的作用就是唤醒waiting状态的goroutine

carbon (20).png 还是通过systemstack切到g0栈,在g0栈上发起调度

carbon (21).png

  1. 获取goroutine的状态
  2. 将waiting状态的goroutine切换到runable状态
  3. 尝试唤起一个p来执行当前goroutine