Go-GMP调度
用户态线程
GMP
-
-
M:表示 Machine,抽象化代表内核线程,记录内核线程栈信息
当 goroutine 调度到线程时,使用该 goroutine 自己的栈信息。 -
P: processor 代表调度器,负责调度 goroutine
在 Go 1.5 及之前的版本中,默认情况下,m 的最大数量为 GOMAXPROCS(默认为 CPU 核心数量),p 的最大数量为 256。在 Go 1.6 及之后的版本中,这些数量已经不再有硬编码的限制,而是根据运行环境动态调整,以获得更好的性能表现。
在src/runtime/proc.go 中可以看到:
sched.maxmcount = 10000 // maximum number of m's allowed (or die) m的最大数量
上图说明
sudog阻塞队列
每个阻塞操作都会创建一个对应类型的 sudog,并将其插入到对应类型的等待队列中进行等待。当该操作可以被执行时,就会从等待队列中取出对应的 sudog,并将其唤醒,使其重新进入可执行状态,以继续执行原来的代码。
sudog 实际上是一个通用的同步数据结构,Go 中有多种类型的 sudog,每种类型的 sudog 都对应着不同的同步操作。
sudog 主要有以下几种类型:
send
sudog:用于实现向 chan 发送数据的同步操作;recv
sudog:用于实现从 chan 接收数据的同步操作;select
sudog:用于实现 select 语句中的多路复用;timer
sudog:用于实现定时器相关的同步操作;semacquire
sudog:用于实现信号量的同步操作;mutex
sudog:用于实现互斥锁的同步操作。
每种 sudog 都有自己独有的属性和方法,用于实现特定的同步操作。例如,send
sudog 包含了发送的数据和目标 chan 的指针,recv
sudog 则包含了接收数据的指针和目标 chan 的指针等。每个 sudog 都被插入到对应的等待队列中,等待被唤醒执行。
需要注意的是,虽然每种 sudog 都对应着不同的同步操作,但它们的底层实现都是相同的,都是通过锁和条件变量实现的。只是在不同的场景下,使用不同类型的 sudog 实现不同类型的同步操作而已。
gFree(全局自由 G 列表)
gFree 列表是一个全局的数据结构,被所有的 P(processor)所共享。当一个 P 需要创建一个新的 goroutine 时,它会首先尝试从本地的 gFree 列表中取出一个空闲的 G,如果本地列表为空,则会尝试从全局的 gFree 列表中获取。如果全局列表也为空,那么 P 就会根据需要创建一个新的 G。
总之,gFree 列表是 GMP 中的一个重要数据结构,用于存储和管理空闲的 G。它可以减少 G 的创建和销毁的开销,提高 goroutine 的创建和执行效率。
pidle(全局空闲P列表)
pidle 列表则是用于存储空闲的 P 的,当一个 P 不再需要执行任务时,它会将自己放入到 pidle 列表中,等待下次被复用。当需要一个新的 P 时,就可以从 pidle 列表中取出一个空闲的 P,如果pidle列表为空,则会尝试创建一个新的 P。
需要注意的是,pidle 列表是一个全局的数据结构,被所有的 P 所共享。当一个 P 需要创建或释放 goroutine 时,它会首先尝试从本地的 pidle 列表中获取或放入一个 P,如果本地列表为空,则会尝试从全局的 pidle 列表中获取或放入一个 P。这样可以避免在创建和销毁 P 的时候产生过多的开销,提高 goroutine 的执行效率。
G 所处的位置
-
-
每个 P 拥有自己的本地执行队列
-
有不在运行队列中的 G
-
处于 channel 阻塞态的 G 被放在 sudog
-
脱离 P 绑定在 M 上的 G,如系统调用
-
-
-
_Grunnable:没有执行代码,没有栈的所有权,存储在运行队列中
-
_Grunning:可以执行代码,拥有栈的所有权,被赋予了内核线程 M 和处理器 P
-
_Gsyscall:正在执行系统调用,拥有栈的所有权,没有执行用户代码,被赋予了内核线程 M 但是不在运行队列上
-
_Gwaiting:由于运行时而被阻塞,没有执行用户代码并且不在运行队列上,但是可能存在于 Channel 的等待队列上
-
_Gdead:没有被使用,没有执行代码,可能有分配的栈
-
_Gcopystack:栈正在被拷贝,没有执行代码,不在运行队列上
-
_Gpreempted:由于抢占而被阻塞,没有执行用户代码并且不在运行队列上,等待唤醒
-
_Gscan
Goroutine 创建过程
-
-
从处理器的 gFree 列表中查找空闲的 Goroutine
-
如果不存在空闲的 Goroutine,会通过 runtime.malg 创建一个栈大小足够的新结构体
-
-
将函数传入的参数移到 Goroutine 的栈上
-
更新 Goroutine 调度相关的属性,更新状态为_Grunnable
-
将 Goroutine 放到运行队列上
-
Goroutine 设置到处理器的 runnext 作为下一个处理器 执行的任务
-
当处理器的本地运行队列已经没有剩余空间时(好像是256大小),就会把 本地队列中的一部分 Goroutine 和待加入的 Goroutine 通过 runtime.runqputslow 添加到调度器持有的全局
调度器行为
-
为了保证公平,当全局运行队列中有待执行的 Goroutine 时,通过 schedtick 保证有一定 几率(1/61)会从全局的运行队列中查找对应的 Goroutine
-
从处理器本地的运行队列中查找待执行的 Goroutine
-
如果前两种方法都没有找到 Goroutine,会通过 runtime.findrunnable 进行阻塞地查找 Goroutine
-
从本地运行队列、全局运行队列中查找
-
从网络轮询器中查找是否有 Goroutine 等待运行
-
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律