七叶笔记 » golang编程 » go高级进阶:goroutine的创建、休眠与恢复

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

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再去调度…

goroutine的创建

通过 go tool compile -S main.go 我们来看看发生了什么?

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

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

  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。

gopark的作用:

  1. 将running状态的goroutine设置为waiting
  2. 解除goroutine和当前工作线程M的关系
  3. 获取一个新goroutine来运行

gopark函数的关键就是mcall函数调用的park_m。

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

还是通过systemstack切到g0栈,在g0栈上发起调度

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

欢迎大家关注公众号《假装懂编程》,我将持续输出网络、数据库、go、缓存、架构、面试、程序人生相关文章。

相关文章