首页 > 其他分享 >go的GPM - 协程的本质

go的GPM - 协程的本质

时间:2023-11-30 12:22:05浏览次数:35  
标签:GPM 协程 gp gobuf 线程 func go MOVQ

协程与线程

线程在创建、切换、销毁时候,需要消耗CPU的资源。

协程就是将一段程序的运行状态打包, 可以在线程之间调度。减少CPU在操作线程的消耗

进程用分配内存空间
线程用来分配CPU时间
协程用来精细利用线程
协程的本质是一段包含了运行状态的程序  后面介绍后,会对这个概念更好理解

协程的本质

上面讲了 ,协程的本质就是 一段程序的运行状态的打包:

  func Do() {
  	for i := 1; i <= 1000; i++ {
  		fmt.Println(i)
  		time.Sleep(time.Second)
  	}
  }

  func main() {
  	go Do()
  	select {}
  }

例如上面这段代码,开了一个协程,然后一直循环打印。

假设程序都还有很多其他的协程也在工作,发现这个协程工作太久了,系统会进行切换别的协程,现在这个协程会放入协程队列中。

问题:要做到这点,协程需要怎么保存这个执行状态?

  1. 需要一个函数的调用栈,记录执行了那些函数,(例子中只有一个,正常情况下会是很多函数相互调用) 函数执行完后,还需要回到上层函数,所以要保存函数栈信息。
  1. 需要记录当前执行到了 那行代码,不能把多执行,也不能少执行那句代码,不然程序会不可控。
  1. 需要一个空间,存储整个协程的数据,例如变量的值等。

协程的底层定义

在runtime的runtim2.go中

  type g struct {
  // 只留了少量几个,里面有非常多的字段。
  	stack       stack  // 调用栈
  	m         *m        // 协程关联了一个m (GMP)
        sched     gobuf  // 协程的现场
  	goid         uint64   // 协程的编号
      atomicstatus atomic.Uint32 // 协程的状态

  }

type gobuf struct {
       sp   uintptr  // 当前调用的函数
	pc   uintptr  // 执行语句的指针
	g    guintptr
	ctxt unsafe.Pointer
	ret  uintptr
	lr   uintptr
	bp   uintptr // for framepointer-enabled architectures
}

// 栈的定义
  type stack struct {
  	lo uintptr  // 低地址
  	hi uintptr  // 高地址
  }

整体下:

假如有这么一段代码:

  func do3() {
  	fmt.Println("dododo")
  }

  func do2() {
  	do3()
  }

  func do1() {
  	do2()
  }

  func main() {
  	go do1()
  	time.Sleep(time.Hour)
  }

在do2断点:

能看到下方的调用栈中,会自动插入一个 goexit 在栈头。

小结下,整体的结构如下:

总结:

runtime 中,协程的本质是一个g 结构体
stack:堆栈地址
gobuf:目前程序运行现场
atomicstatus: 协程状态

线程的底层 m

操作系统的线程是由操作系统管理,这里的m只是记录线程的信息。

截取部分代码:
type m struct {
	g0      *g     // goroutine with scheduling stack
	id            int64 // id号
	morebuf gobuf  // gobuf arg to morestack	
	curg          *g       // 当前运行的g
	p             puintptr // attached p for executing go code (nil if not executing go code)
	mOS // 系统线程信息
}

go 是go程序启动创建的第一个协程,用来操控调度器的,第二个是主协程,可以看下 go启动那篇

小结:

runtime 中将操作系统线程抽象为 m结构体
g0:g0协程,操作调度器
curg:current g,目前线程运行的g
mOs:操作系统线程信息

如何工作

协程究竟是如何在 线程中工作的 ?

先讲总结,然后跟着总结往下看:

这是单个线程的循环,没有P的存在。

1. schedule() 是线程获取 协程的入口方法

线程通过执行 g0协程栈,获取 待执行的 协程

也就是意味着,每次线程执行 这个schedule方法,就意味着会切换一个 协程。
这个结论很重要,后面 协程调度时候,会大量看到调用这个方法。

在runtime的 proc.go下面能看到这个方法,这里只留了两行代码,
只和目前逻辑相关的,这个方法后面还要多次读
   func schedule() {
  	gp, inheritTime, tryWakeP := findRunnable() // blocks until work is available
  	execute(gp, inheritTime)
  }
这里的gp就是 待执行的g

 可以和上面的图对上,这里去  `Runnable` 找一个协程。然后,调用 `execute` 方法。
 至于怎么去找的,知道GMP的肯定都知道,这个后面聊。

也只有部分代码,和这里业务相关的
 func execute(gp *g, inheritTime bool) {
  	mp := getg().m  //获取m,线程的抽象
  	mp.curg = gp   // 还记得 m的定义 里面有个 当前的 g 在这里赋值了
  	gp.m = mp      // g的定义也有个 m,这里也赋值了
  	gogo(&gp.sched)
  }

到gogo
func gogo(buf *gobuf) // 只有定义,说明是汇编实现的,而且是平台相关的
     
// func gogo(buf *gobuf)  
// 这里把 g的gobuf传过去了,gobuf 存着 sp 和 pc ,当前的执行函数,和执行语句
//  到这里就基本对应上了
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $0-8
	MOVQ	buf+0(FP), BX		// gobuf
	MOVQ	gobuf_g(BX), DX
	MOVQ	0(DX), CX		// make sure g != nil
	JMP	gogo<>(SB)

//  插入了 goexit的栈针 然后开始运行业务 
TEXT gogo<>(SB), NOSPLIT, $0
	get_tls(CX)
	MOVQ	DX, g(CX)
	MOVQ	DX, R14		// set the g register
	MOVQ	gobuf_sp(BX), SP	// restore SP 插入了 goexit的栈针
	MOVQ	gobuf_ret(BX), AX
	MOVQ	gobuf_ctxt(BX), DX
	MOVQ	gobuf_bp(BX), BP
	MOVQ	$0, gobuf_sp(BX)	// clear to help garbage collector
	MOVQ	$0, gobuf_ret(BX)
	MOVQ	$0, gobuf_ctxt(BX)
	MOVQ	$0, gobuf_bp(BX)
	MOVQ	gobuf_pc(BX), BX
	JMP	BX

在运行业务之前 jmp bx,都还在 g0的协程栈上。

目前,已经把开始执行,到执行都整理了一遍,但是,没有讲 goexit 插入 到底有什么作用?

经验丰富的伙伴大致能猜到, 当执行完了协程的任务后,需要回到 schedule方法中, 线程重新去执行别的协程,这就是 goexit的作用

goexit

汇编实现
TEXT runtime·goexit(SB),NOSPLIT|TOPFRAME,$0-0
BYTE	$0x90	// NOP
CALL	runtime·goexit1(SB)	//  去调用 goexit1 这个方法

// Finishes execution of the current goroutine.
func goexit1() {
	mcall(goexit0) // 通过mcall 调用goexit0  
}

 // mcall switches from the g to the g0 stack and invokes fn(g),
 // 切换到 g0 栈
 func mcall(fn func(*g))
 就是只,上面的都是在 业务协程中,运行的,到这里,开始使用 g0栈去运行,goexit0

// goexit continuation on g0. 
func goexit0(gp *g) {
	mp := getg().m
	pp := mp.p.ptr()

	casgstatus(gp, _Grunning, _Gdead)
	gcController.addScannableStack(pp, -int64(gp.stack.hi-gp.stack.lo))
	if isSystemGoroutine(gp, false) {
		sched.ngsys.Add(-1)
	}
	gp.m = nil
	locked := gp.lockedm != 0
	gp.lockedm = 0
	mp.lockedg = 0
	gp.preemptStop = false
	gp.paniconfault = false
	gp._defer = nil // should be true already but just in case.
	gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data.
	gp.writebuf = nil
	gp.waitreason = waitReasonZero
	gp.param = nil
	gp.labels = nil
	gp.timer = nil
	schedule()
}
// 对结束的g进行了一些置0的工作,然后调用了 schedule()

schedule() 意味着 为现在的线程,切换协程。

到此,和上面的图都对应上了。但是目前还是单线程,多线程时候,是如何工作了,下篇再聊。

标签:GPM,协程,gp,gobuf,线程,func,go,MOVQ
From: https://www.cnblogs.com/studyios/p/17866765.html

相关文章

  • 安装go
    1.安装包直接安装2.配置环境变量GOPARH:新建的,用来存放go项目代码的地址GOROOT:你安装go的目录 3.创建文件目录在GOPATH地址下面,创建3个文件夹 ......
  • nango 通用api 集成平台
    nango通用api集成平台包含的特性超过100+的api认证可以双向数据同步基于通用api的快速访问自动api限速,重试以及分页自定义模式的强类型支持webhook以及实时数据同步支持内置监控admindshboard访问说明nango提供了好几种模式,免费自托管,云,企业自托管,免费自托......
  • Django学习(一) 之 环境搭建
    写在前面最近比较迷AI绘图,那就上个图吧,我感觉还挺好看的。可能会有人说,之前不一致分享的是flask吗,怎么突然改到django了?这个问题问得好,开发环境遇到了一些小困难!不过django,真的是很流行,一点都不过时,这您放心好了!不多说,直接看效果吧!环境搭建1、当前环境版本python==3.9.1......
  • django中实现事务的几种方式
    django中实现事务的几种方式https://zhuanlan.zhihu.com/p/622987268具体表现形式为:每次数据库操作(比如调用save()方法)会立即被提交到数据库中。但是如果你希望把连续的SQL操作包裹在一个事务里,就需要手动开启事务根据粒度不同,三种全局:全局,每次请求在一个事务中,粒度太大,事......
  • go数据类型-空结构体、空接口、nil
    空结构体funcmain(){ a:=struct{}{} fmt.Println(unsafe.Sizeof(a)) fmt.Printf("%p\n",&a)}打印00x117f4e0有经验的开发人员都知道,所有的空结构体是指向一个zerobase的地址,而且大小为0一般用来作结合map作为set或者在channel中传递信号。t......
  • Django中实现事务的几种方式、事物的回滚和保存点、事务提交后,执行某个回调函数、Djan
    Django中实现事务的几种方式#https://zhuanlan.zhihu.com/p/622987268Django是支持事务操作的,它的默认事务行为是自动提交,具体表现形式为:每次数据库操作(比如调用save()方法)会立即被提交到数据库中。但是如果你希望把连续的SQL操作包裹在一个事务里,就需要手动开启事务#......
  • Django补充3
    Django分了很多层路由曾视图层请求对象和响应对象模板曾模型层:orm:表单,多表,各种查询ajaxforms组件 分页器 cookiesession  中间件 auth————————————————————————————————————————————————......
  • 页面静态化——Django中Template和Context模块的使用方法
    1.Template和Context的导入fromdjango.templateimportTemplate,Context2.生成静态页面——在后端调用模板语法生成HTML页面,并保存到指定路径 2.1我们想生成一个前端页面,代码如下后端视图层传入的对象:user_data=models.Userdata.objects.all()<html......
  • VSCode - Disable go test cache
    or Adding"-count=1"to"go.testFlags"candisablegotestcache.......
  • Golang-常见数据结构实现原理
    chan 1.chan数据结构 src/runtime/chan.go:hchan定义了channel的数据结构:typehchanstruct{qcountuint//当前队列中剩余元素个数dataqsizuint//环形队列长度,即可以存放的元素个数bufunsafe.Pointer//环形队列指针......