深入理解 goroutine 底层原理与 GMP 调度模型

前言

goroutine 协程是 golang 语言比较重要的部分,理解 goroutine 可以帮助我们更好地写出高质量的代码。当然,这个也是面试中经常会问到的,今天我们就来讲讲 goroutine 与 GMP 调度模型。

什么是 goroutine 协程

goroutine 是 go 语言中的协程,可以理解为一种轻量级的线程。那么它与线程、进程又有什么区别呢?

进程线程协程
调度系统调度系统调度用户调度
拥有独立栈拥有独立栈拥有独立栈
拥有独立堆共享堆共享堆
内存占用

一个协程的内存占用仅有 2 KB,还可以动态扩容,而一个线程就要几MB,在内存消耗方面不是一个数量级的。协程之间的切换约为 200ns,线程间的切换时间约为 1000-1200ns。

goroutine 的底层是一个结构体

关于 goroutine 的底层,我们可以查看一下源码:(我的 go 版本 1.17)

/在你的 go 目录下 /go/src/runtime/runtime2.go

这个结构体便是 goroutine 的结构体,由于这个结构体本身的内容还是很多的,我这边省略了一些。作为用户态的线程,那么肯定使用实现一个栈来存储变量,在 goroutine 切换的时候,你要记录上下文吧?便有 sched 这个字段。

type g struct {
	stack       stack   // 协程的 栈,
	sched     gobuf  // 在协程切换的时候,用于保存上下文
	goid         int64  //goroutine 的ID
	gopc           uintptr         // pc of go statement that created this goroutine
	startpc        uintptr         // pc of goroutine function
	_defer    *_defer // defer 指针,指向一个 defer 单链表,每次定义一个 defer 都会插入到单链表头部,每次执行都从从不获取,这样就有后进先出的执行效果。
	此处省略很多参数

对于 defer: defer 指针,指向一个 defer 单链表,每次定义一个 defer 都会插入到单链表头部,每次执行都从从不获取,这样就有后进先出的执行效果。

gobuf 这个结构体用来保存 goroutine 的上下文,用来保存栈指针的位置,程序运行到哪里了?等等

type gobuf struct {
	sp   uintptr //栈指针位置
	pc   uintptr //运行到的程序位置
	g    guintptr //指向 goroutine
	ctxt unsafe.Pointer 
	ret  uintptr //保存系统调用的返回值
	lr   uintptr
	bp   uintptr // for framepointer-enabled architectures
}
type stack struct {
	lo uintptr  //栈的下界内存地址
	hi uintptr  //栈的商界内存地址
}

goroutine 是如何创建的?

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

当我们使用 go 关键字之后,就会调用底层的 runtime.newproc() 函数,创建 goroutine 的同时,也会初始化栈空间,上下文 等信息。

func newproc(siz int32, fn *funcval) {
	argp := add(unsafe.Pointer(&fn), sys.PtrSize)
	gp := getg()
	pc := getcallerpc()
	// 用 g0 创建 g 对象
	systemstack(func() {
		newg := newproc1(fn, argp, siz, gp, pc)

		_p_ := getg().m.p.ptr()
		runqput(_p_, newg, true)

		if mainStarted {
			wakep()
		}
	})
}

goroutine 是如何运行起来的?GMP 调度模型

从上面的解析中,我们可以看出 goroutine 只是一个数据结构,这个结构体里面有栈信息,上下文 等,而真正要把 goroutine 运行起来,就要设计到 GMP 调度模型了。

那 GMP 究竟是什么意思呢?

G:代表 goroutine
M:代表线程
P:代表调度器
在这里插入图片描述

  • 在新创建一个 G 时,会优先进入局部队列,如果局部队列满了,那么会进入全局队列。
  • 当运行中的 G发生系统调用的时候,P 会与 当前的 M 解绑,然后与 M1 结合,继续执行后续的任务,而 M 则与 G 等待系统返回,这个叫 hand off 机制。
  • 当 P 的局部队列里面的 G 运行完毕之后,会用全局队列获取 G,在获取的过程中,会加锁。如果全局队列为空,则从其他 P 的局部队列中 偷一半的 G 过来运行,这个叫 work stealing 机制。
  • 当运行中的 G1 发生 IO 调用或者网络调用,P 则会继续调度下一个 G2,等 G1 网络调用、IO 调用结束,则会重新返回局部队列的队尾。
  • 每个 M 默认会搭配一个 G0,用来调度 G,当 P 队列里面的 G 执行完毕之后,M 会进入自旋状态,即是不断地寻找可执行的 G。

关于 P 和 M 的数量

P 的数量可以通过环境变量 $GOMAXPROCS 和 runtime.GOMAXPROCS() 来设置
M 的数量默认是 10000,但是太多也没有用,还得看你的 CPU 有多少内核,才能充分利用。

P 和 M 的数量没有固定的搭配关系,当一个 M 阻塞了,P 会发生 hand off,与其他空闲的 M 结合,或者产生新的 M。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值