G 待执行的goroutine,结构定义在runtime.g
M 操作系统中的线程,它由操作系统的调度器 进行 调度和管理, 结构定义在runtime.m
P 处理器,是GM的中间件,它通过一个队列绑定了GM,每个P都有一个局部queue,用来存放待执行的本地goroutine
Goroutine 只存在于 Go 语言的运行时,它是 Go 语言在用户态提供的线程,作为一种粒度更细的资源调度单元。
Goroutine 在 Go 语言运行时使用私有结构体 runtime.g
表示。这个私有结构体非常复杂,总共包含 40 多个用于表示各种状态的成员变量。
runtime.g存放在内存堆上,对所有线程都共享
栈相关字段
type g struct {
stack stack
stackguard0 uintptr
}
StackPreempt
意味着当前 Goroutine 发出了抢占请求;抢占式调度字段
type g struct {
preempt bool // 抢占信号
preemptStop bool // 抢占时将状态修改成 `_Gpreempted`
preemptShrink bool // 在同步安全点收缩栈
}
defer和panic链表字段,链表头插入,链表头获取
type g struct {
_panic *_panic // 最内侧的 panic 结构体
_defer *_defer // 最内侧的延迟函数结构体
}
调度相关
type g struct {
m *m
sched gobuf
atomicstatus uint32
goid int64
}
状态 | 描述 |
---|---|
_Gidle | 刚被分配,但未被初始化 |
_Grunnable | 未执行代码,没有栈的所有权,存储在运行队列中 |
_Grunning | 可以执行代码,拥有栈的所有权,被赋予了内核线程M和处理器P |
_Gsyscall | 正在执行系统调用,拥有栈的所有权,没有执行用户代码,被赋予了内核线程M但是不在运行队列上 |
_Gwaiting | 由于运行时而被阻塞,没有执行用户代码并且不再运行队列上,但是可能存在于channel的等待队列上 |
_Gdead | 没有被使用,没有执行代码,可能有分配的栈 |
_Gcopystack | 栈正在拷贝,没有执行代码,不在运行队列上 |
_Greempted | 由于抢占而被阻塞,没有执行用户代码并且不在运行队列上,等待唤醒 |
_Gscan | GC正在扫描空间,没有执行代码,可以于其他状态同时存在 |
_Gwaiting
、_Gsyscall
和 _Gpreempted
几个状态;_Grunnable
;_Grunning
; Go 语言并发模型中的 M 是操作系统内核线程。调度器最多可以创建 10000 个线程,但是其中大多数的线程都不会执行用户代码(可能陷入系统调用),最多只会有 GOMAXPROCS
个活跃线程能够正常运行(因为运行时需要绑定P,P的数量是由GOMAXPROCS
决定的)。
在默认情况下,一个四核机器会创建四个活跃的操作系统线程,每一个线程都对应一个运行时中的 runtime.m
结构体。
在大多数情况下,我们都会使用 Go 的默认设置,也就是线程数等于 CPU 数,默认的设置不会频繁触发操作系统的线程调度和上下文切换,所有的调度都会发生在用户态,由 Go 语言调度器触发,能够减少很多额外开销。
结构体runtime.m
表示操作系统线程,这个结构体也包含了几十个字段
与协程相关字段:
type m struct {
g0 *g
curg *g
...
}
与处理器P相关字段:
type m struct {
p puintptr
nextp puintptr
oldp puintptr
}
调度器中的处理器 P 是线程和 Goroutine 的中间层,它能提供线程需要的上下文环境,也会负责调度线程上的等待队列(runq本地队列),通过处理器 P 的调度,每一个内核线程都能够执行多个 Goroutine,它能在 Goroutine 进行一些 I/O 操作时及时让出计算资源,提高线程的利用率。
因为调度器在启动时就会创建 GOMAXPROCS
个处理器,所以 Go 语言程序的处理器数量一定会等于 GOMAXPROCS
,这些处理器会绑定到不同的内核线程上。
type p struct {
m muintptr
runqhead uint32
runqtail uint32
runq [256]guintptr
runnext guintptr
...
}
处理器P状态:
状态 | 描述 |
---|---|
_Pidle | 处理器没有运行用户代码或者调度器,被空闲队列或者改变其状态的结构持有,运行队列为空 |
_Prunning | 被线程 M 持有,并且正在执行用户代码或者调度器 |
_Psyscall | 没有执行用户代码,当前线程陷入系统调用 |
_Pgcstop | 被线程 M 持有,当前处理器由于垃圾回收被停止 |
_Pdead | 当前处理器已经不被使用 |
通过分析处理器 P 的状态,我们能够对处理器的工作过程有一些简单理解,例如处理器在执行用户代码时会处于 _Prunning
状态,在当前线程执行 I/O 操作时会陷入 _Psyscall
状态。
步骤:
TLS(thread local storage)是每个线程私有的存储空间
静态数据段:是存放全局变量的,是所有thread公用的内存段,所以需要加锁来保证线程安全
每一个m创建时都会进行调度器初始化。以下是第一个m0的初始化步骤:
allp[]
全局变量__Pidel__
状态allgs[]
、allm[]
、sched
等调用newproc()方法来创建main函数的协程,加入到m0本地队列的P中
以下面例子做协程创建
package main
func helle(name string) {
fmt.Println("Hello ", name)
}
func main() {
name := "Goroutine"
go hello(name) // 调用newproc()创建协程
}
以下是代码函数栈帧的变化,栈内变量是由高到底存放“
执行main函数,创建main函数的函数栈帧:
执行name:="Goroutine"
,存放调用者caller(main函数)的局部变量
执行go hello(name)
,准成机器码就是执行newproc(siz int32, fn *funcval)
。变量由右至左存放到栈 siz变量, fn变量 和 参数变量name。main函数栈帧结束
调用newproc的返回地址(调用一个函数叫做call func),指向hello()
开始newprocd的栈基,BP of newproc
切换到g0栈(线程里的栈帧)。调用newproc1()函数。因为线程栈比协程栈要大,防止栈溢出
newproc1()会先将当前m锁住,runtime.gfget
方法获取过两种不同的方式获取新的 runtime.g
gFree
列表或者调度器的 sched.gFree
列表中获取 runtime.g
);空协程,可是已经分配了栈内存空间,避免g0的切换和g的重复销毁/创建runtime.malg
生成一个新的 runtime.g
并将结构体追加到全局的 Goroutine 列表 allgs
中。runtime.g
结构体保存在堆上(因为要线程共享),runtime.g.stack
指向协程函数funcval
的函数栈帧。runtime.mstart
以及 runtime.mstart1
,前者会初始化 g0 的 stackguard0
和 stackguard1
字段,后者会初始化线程并调用 runtime.schedule
进入调度循环。runtime.findrunnabel
,阻塞查找goroutine,通过以下的过程获取可运行的 Goroutine:
runtime.runqsteal
尝试从其他随机的处理器中窃取待运行的 Goroutine,该函数还可能窃取处理器的计时器;runtime.execute
执行获取的 Goroutine,做好准备工作后,它会通过 runtime.gogo
将 Goroutine 调度到当前线程上。runtime.goexit0
函数,该函数会将 Goroutine 转换会 _Gdead
状态、清理其中的字段、移除 Goroutine 和线程的关联并调用。重新加入处理器的 Goroutine 空闲列表 gFree
。返回1步骤重新一轮新的调度runtime.gopark
切换到g0触发调度是最常见的方法,他会将正在运行的goroutine暂停,不会扔回runq
中,状态从_Gruning
变成_GWaiting
runtime.goready
将协程从_Gwaiting
状态切换成_Grunable
加入到本地队列系统调用也会触发运行时调度器的调度,为了处理特殊的系统调用,Goroutine 中加入了 _Gsyscall
状态。系统调用会让M和P分离,释放P,让其他空闲M绑定P。
runtime.entersyscall
会在获取当前程序计数器和栈位置之后调用 runtime.reentersyscall
,它会完成 Goroutine 进入系统调用前的准备工作:
_Gsyscall
;_Psyscall
;当系统调用结束后,会调用退出系统调用的函数 runtime.exitsyscall
为当前 Goroutine 重新分配资源,该函数有两个不同的执行路径:
runtime.exitsyscallfast
:获取P的方式来恢复
_Psyscall
,直接使用原处理器sched.pidle
获取一个空闲p。runtime.exitsyscall0
:此方法是无法获取到可用P才会调用,主要就是将G状态变成_Grunable
,扔到全局变量runq
,由调度器去处理Go 语言基于协作式和信号的两种抢占式调度,这里主要介绍其中的协作式调度。runtime.Gosched
函数会主动让出处理器,允许其他 Goroutine 运行。该函数无法挂起 Goroutine,调度器可能会将当前 Goroutine 调度到其他线程上。
最终在 g0 的栈上调用 runtime.goschedImpl
,运行时会更新 Goroutine 的状态到 _Grunnable
,让出当前的处理器并将 Goroutine 重新放回全局队列,在最后,该函数会调用 runtime.schedule
触发调度。
Go 语言的运行时会通过 runtime.startm
启动线程来执行处理器 P,如果我们在该函数中没能从闲置列表中获取到线程 M 就会调用 runtime.newm
创建新的线程:
clone
创建的线程会在线程主动调用 exit
、或者传入的函数 runtime.mstart
返回会主动退出,runtime.mstart
会执行调用 runtime.newm
时传入的匿名函数 fn
,到这里也就完成了从线程创建到销毁的整个闭环。
Golang调度器GMP原理与调度全分析
Go语言设计与实现
幼麟go系列