Go 的线程实现模型,有三个核心的元素 M、P、G,它们共同支撑起了这个线程模型的框架。其中,G 是 goroutine 的缩写,通常称为 “协程”。关于协程、线程和进程三者的异同,可以参照 “进程、线程和协程的区别”。
每一个 Goroutine 在程序运行期间,都会对应分配一个 g 结构体对象。g 中存储着 Goroutine 的运行堆栈、状态以及任务函数,g 结构的定义位于 src/runtime/runtime2.go 文件中。
g 对象可以重复使用,当一个 goroutine 退出时,g 对象会被放到一个空闲的 g 对象池中以用于后续的 goroutine 的使用,以减少内存分配开销。
1. Goroutine 字段注释
g 字段非常的多,我们这里分段来理解:
type g struct {
// Stack parameters.
// stack describes the actual stack memory: [stack.lo, stack.hi).
// stackguard0 is the stack pointer compared in the Go stack growth prologue.
// It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
// stackguard1 is the stack pointer compared in the C stack growth prologue.
// It is stack.lo+StackGuard on g0 and gsignal stacks.
// It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
stack stack // offset known to runtime/cgo
// 检查栈空间是否足够的值, 低于这个值会扩张, stackguard0 供 Go 代码使用
stackguard0 uintptr // offset known to liblink
// 检查栈空间是否足够的值, 低于这个值会扩张, stackguard1 供 C 代码使用
stackguard1 uintptr // offset known to liblink
}
stack 描述了当前 goroutine 的栈内存范围[stack.lo, stack.hi),其中 stack 的数据结构:
// Stack describes a Go execution stack.
// The bounds of the stack are exactly [lo, hi),
// with no implicit data structures on either side.
// 描述 goroutine 执行栈
// 栈边界为[lo, hi),左包含右不包含,即 lo≤stack<hi
// 两边都没有隐含的数据结构。
type stack struct {
lo uintptr // 该协程拥有的栈低位
hi uintptr // 该协程拥有的栈高位
}
stackguard0 和 stackguard1 均是一个栈指针,用于扩容场景,前者用于 Go stack ,后者用于 C stack。
如果 stackguard0 字段被设置成 StackPreempt,意味着当前 Goroutine 发出了抢占请求。
在g结构体中的stackguard0 字段是出现爆栈前的警戒线。stackguard0 的偏移量是16个字节,与当前的真实SP(stack pointer)和爆栈警戒线(stack.lo+StackGuard)比较,如果超出警戒线则表示需要进行栈扩容。先调用runtime·morestack_noctxt()进行栈扩容,然后又跳回到函数的开始位置,此时此刻函数的栈已经调整了。然后再进行一次栈大小的检测,如果依然不足则继续扩容,直到栈足够大为止。
type g struct {
preempt bool // preemption signal, duplicates stackguard