深入理解 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
	此处省略很多参数

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:代表调度器
深入理解 goroutine 底层原理与 GMP 调度模型_第1张图片

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

你可能感兴趣的