Go调度系列--goroutine和调度器生命周期(三)

2023-03-24 11:10:33 浏览数 (1)

前言

调度器schedule和goroutine的生命周期其实在整个go程序中有着极其重要的地位,几乎贯穿go程序的一生,在Go调度系列(二)中,我们把Go调度器的运转原理理了一遍,知道调度器是如何进行调度。那么 goroutine 是怎么诞生然后被调度的呢?

在Go中创建一个 goroutine特别容易,go 函数名( 参数列表 )就可以了。

代码语言:javascript复制
func main() {
 go func() {
  fmt.Println("小许code,开启一个协程")
 }()
}

go 关键字创建 goroutine 时,被调用函数的返回值会被忽略,如果需要在 goroutine 中返回数据,可以通过channel把数据从 goroutine 中作为返回值传出。

go func()经历了哪些流程

go func() 经历了哪些流程,其实我们可以理解为goroutine的生命周期,表示goroutine从创建 --> 入队列 --> 被调度 这些流程。当我们使用关键字go的时候实际是调用的 runtime/newproc()函数,创建 goroutine 的同时,也会初始化栈空间,上下文 等信息。

代码语言:javascript复制
func newproc(fn *funcval) {
 gp := getg() //返回指向当前g的指针
 pc := getcallerpc()  //返回其调用者的程序计数器(PC),堆栈指针(SP)
 systemstack(func() {
  newg := newproc1(fn, gp, pc)  //创建一个_Grunnable状态的新g

  _p_ := getg().m.p.ptr()
  runqput(_p_, newg, true)  // 将新产生的goroutine放在当前P的可执行队列中

  if mainStarted { // 表示主M已启动
   wakep()  //尝试再添加一个P来执行G。当G可运行时调用
  }
 })
}

我们看runqput(_p_, newg, true),函数的注释部分就讲清楚了goroutine怎么判断和放哪个位置的。其实这里就是将goroutine创建和跟p绑定,然后存放位置确定,然后等待调度器调度执行。调度器schedule() 可以在《调度实现原理》这篇中找到实现逻辑。

代码语言:javascript复制
// runqput tries to put g on the local runnable queue.
// If next is false, runqput adds g to the tail of the runnable queue.
// If next is true, runqput puts g in the _p_.runnext slot.
// If the run queue is full, runnext puts g on the global queue.
// Executed only by the owner P.
func runqput(_p_ *p, gp *g, next bool) {
 if randomizeScheduler && next && fastrandn(2) == 0 {
  next = false
 }
 ...
}

结合goroutine的产生和调度流程,下图能较清楚的表示整个流程:

编辑切换为居中

添加图片注释,不超过 140 字(可选)

整个流程分为 ‘goroutine创建’ 和 ‘schedule调度‘ 两个阶段

创建阶段

1.创建一个G

2.1G优先放到当前线程持有的P的本地队列中;

2.2如果已经满了,则放入全局队列中

3.1M通过P获取G;(一个M必须持有一个P——1:1)

3.2如果M的本地队列为空,从全局队列获取G;

3.3(work stealing机制)如果也为空,则从其他的MP组合偷取G

调度阶段

4:调度

5.执行func()函数

5.1 超出时间片后返回P的本地队列

5.2 若G.func()发生systemCall/阻塞

5.2.1 runtime(即调度器)会把这个M从P中摘除,(hand off机制)创建一个M或从休眠队列中取一个空闲的M,接管正在被阻塞中的P

5.2.2 M系统调用(阻塞)结束时,G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态,加入到空闲线程中,然后这个G会被放入全局队列中

6.销毁G

m0

什么是m0和g0,有什么作用,和m、g有什么区别呢?

先看m0,m0是Go runtime创建的第一个系统线程(主线程),一个Go进程只有一个m0。

数据结构:从数据结构上看m0和其他m没有区别,同属于m结构体, m0的定时是 var m0 m。

创建:m0是进程在启动时由汇编创建m0的,而其他的m是Go运行时创建,m0在全局变量runtime.m0中,不需要在heap上分配。

作用:负责执行初始化操作和启动第一个G,启动第一个G后,M0就和其他的一样了

g0

goroutine在程序中一般分为三种:执行用户任务的g、执行 runtime.main的main goroutine、执行任务调度的g0。

g0具有线程唯一性(一个线程m中唯一),每次启动一个M,都会第一个创建g0,每个M都会有一个自己的g0,但是g0不会指向执行函数。

调度g:G0仅用于负责调度其他的G(M可能会有很多的G,然后G0用来保持调度栈的信息),当一个M从G1切换到G2,首先应该切换到G0,通过G0把G1干掉,再切换到G2

存放空间:M0的G0会放在全局空间

调度器生命周期

再来看go调度器的生命周期,刚好看到有对于调度器生命周期的流程图。

编辑

添加图片注释,不超过 140 字(可选)

  1. runtime 创建最初的线程 m0 和 goroutine g0,并把 2 者关联。
  2. 调度器初始化:初始化 m0、栈、垃圾回收,以及创建和初始化由 GOMAXPROCS 个 P 构成的 P 列表。
  3. 示例代码中的 main 函数是 main.main,runtime 中也有 1 个 main 函数 ——runtime.main,代码经过编译后,runtime.main 会调用 main.main,程序启动时会为 runtime.main 创建 goroutine,称它为 main goroutine 吧,然后把 main goroutine 加入到 P 的本地队列。
  4. 启动 m0,m0 已经绑定了 P,会从 P 的本地队列获取 G,获取到 main goroutine。
  5. G 拥有栈,M 根据 G 中的栈信息和调度信息设置运行环境
  6. M 运行 G 7.G 退出,再次回到 M 获取可运行的 G,这样重复下去,直到 main.main 退出,runtime.main 执行 Defer 和 Panic 处理,或调用 runtime.exit 退出程序。 调度器的生命周期几乎占满了一个 Go 程序的一生,runtime.main 的 goroutine 执行之前都是为调度器做准备工作,runtime.main 的 goroutine 运行,才是调度器的真正开始,直到 runtime.main 结束而结束。

0 人点赞