goroutine 协程是 golang 语言比较重要的部分,理解 goroutine 可以帮助我们更好地写出高质量的代码。当然,这个也是面试中经常会问到的,今天我们就来讲讲 goroutine 与 GMP 调度模型。
goroutine 是 go 语言中的协程,可以理解为一种轻量级的线程。那么它与线程、进程又有什么区别呢?
进程 | 线程 | 协程 | |
---|---|---|---|
调度 | 系统调度 | 系统调度 | 用户调度 |
栈 | 拥有独立栈 | 拥有独立栈 | 拥有独立栈 |
堆 | 拥有独立堆 | 共享堆 | 共享堆 |
内存占用 | 高 | 高 | 低 |
一个协程的内存占用仅有 2 KB,还可以动态扩容,而一个线程就要几MB,在内存消耗方面不是一个数量级的。协程之间的切换约为 200ns,线程间的切换时间约为 1000-1200ns。
关于 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 //栈的商界内存地址
}
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 只是一个数据结构,这个结构体里面有栈信息,上下文 等,而真正要把 goroutine 运行起来,就要设计到 GMP 调度模型了。
那 GMP 究竟是什么意思呢?