前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go调度系列--goroutine和调度器生命周期(三)

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

原创
作者头像
小许code
发布2023-03-24 11:10:33
9630
发布2023-03-24 11:10:33
举报
文章被收录于专栏:小许code小许code小许code

前言

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

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

func main() {
 go func() {
  fmt.Println("小许code,开启一个协程")
 }()
}

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

go func()经历了哪些流程

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

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() 可以在《调度实现原理》这篇中找到实现逻辑。

// 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 结束而结束。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • go func()经历了哪些流程
    • 创建阶段
      • 调度阶段
      • m0
      • g0
      • 调度器生命周期
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档