前言
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。