Golang GMP调度模型详解
文章目录
- 前言
- 1. Goroutine调度器的基本概念
- 2. GMP 数据结构
- 2.1 G
- 2.2 M
- 2.3 P
- 3. M缓冲池
- 4. 调度策略
- 4.1 work stealing机制
- 4.2 hand off 机制
- 4.3 抢占
- 4.4 阻塞的两种情况
- 4.5 拓展
- 5. go func()调度流程
- 6. P和M的个数
- 6.1 P的数量
- 6.2 M的数量
- 6.3 P和M什么时候会被创建
- 6.4 问题?
前言
线程数过多,意味着操作系统会不断地切换线程,频繁的上下文切换就成了性能瓶颈。
Golang的调度模型是GMP模型,它提供一种机制,可以在线程中自己实现调度,上下文切换更轻量,从而达到了线程数少,而并发数并不少的效果。而线程中调度的就是Goroutine.
调度的机制用一句话描述就是:runtime准备好G,M,P,然后M绑定P,M从本地或者是全局队列中获取G,然后切换到G的执行栈上执行G上的任务函数,调用goexit做清理工作并回到M,如此反复
接下来我来分模块介绍一下Golang的GMP模型及创建流程
1. Goroutine调度器的基本概念
G(goroutine)
- 即Go协程,每个go关键字都会创建一个协程,它存储了goroutine的执行stack信息(运行时栈信息)、goroutine状态以及goroutine的任务函数等
- 在G眼中只有P,P就是运行G的
CPU
M(machine)
- 工作线程,在Go中称为Machine。
- M是真正调度系统的执行者,它会优先从关联的 P 的本地队列中直接获取中可运行的G,如果本地队列没有的话, 再到调度器持有的全局队列中领取一些任务或是向其他的MP组合偷一半可以执行的G来执行,M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。
P(processor)
- processor处理器,它包含了运行 goroutine 的资源,
- 它用于处理M与G的关系:如果线程想运行 goroutine,必须先获取 P,P 中还包含了可运行的 G 队列
- P的个数在程序启动时决定,默认等同与CPU的核数,通过
runtime.GOMAXPROCS()
设置P的个数
M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队列,P可以调度G交由M执行
2. GMP 数据结构
g、m、p数据结构均在
runtime/runtime2.go
下
2.1 G
g 的关键字段
type g struct {stack stack // 当前G的栈范围stackguard0 uintptr // 判读当前G是否被抢占preempt bool // 抢占信号preemptStop bool // 抢占时将状态修改成 `_Gpreempted`preemptShrink bool // 在同步安全点收缩栈_panic *_panic // 最内侧的 panic 结构体_defer *_defer // 最内侧的延迟函数结构体m *m // 当前G占用的线程sched gobuf // 调度相关数据的存储atomicstatus uint32 // G的状态
}
2.2 M
m的关键字段
- p最多可以创建10000个线程
- 最多只有GOMAXPROCS个活跃线程(与核数一致),这样不会频繁地切换线程上下文
type m struct {g0 *g // 调度栈 使用的Gcurg *g // 当前在M上运行的Gp puintptr // 正在运行代码的Pnextp puintptr // 暂存的Poldp puintptr // 之前使用的P
}
2.3 P
p的关键字段
type p struct {m muintptr // 调度的Mrunqhead uint32 // G队列头runqtail uint32 // G队列尾runq [256]guintptr // G队列runnext guintptr // 下一个可运行的Gstatus int // 当前P的状态
}
状态取值:
- _Pidle:运行队列为空,没有需要运行的G
- _Prunning:M正在执行用户G
- _Psyscall:M处于系统调用
- _Pgcstop:M处于GC垃圾回收的stop中
- _Pdead:P不再被使用
3. M缓冲池
在介绍GMP概念的时候说到:P的个数默认等于CPU核数,每个M必须持有一个P才可以执行G,一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用。类似线程池,Go也提供一个M的池子,需要时从池子中获取,用完放回池子,不够用时就再创建一个。
4. 调度策略
复用线程:避免频繁的创建、销毁线程,而是对线程的复用。
4.1 work stealing机制
当M没有可运行的 G 时,尝试从其他线程M绑定的 P 偷取一半的G过来,而不是销毁线程。
work stealing机制触发:当前M线程的P本地队列中没有可运行的G时 并且 全局队列G中也没有可运行的G时,则会执行workstealing机制.
即:本地队列→\rightarrow→全局队列→\rightarrow→窃取
4.2 hand off 机制
当M阻塞时,M释放绑定的 P(MP分离),把 P 转移给其他空闲的线程执行。
4.3 抢占
在 coroutine 中要等待一个协程主动让出 CPU 才执行下一个协程,在 Go 中,一个 goroutine 执行的时间不能超过 10ms,防止其他 goroutine 被饿死。
4.4 阻塞的两种情况
用户态阻塞/唤醒
例如网络IO、阻塞式channel、sleep等场景(简单来说就是CPU这时候对于这个协程没有事情要做),对于这类阻塞会将G暂时挂起到某一临时等待队列中,待阻塞结束后重新寻找P放入。
系统调用阻塞
M 执行某一个 G 时,如果发生系统调用或则其余阻塞操作,M 会阻塞,如果当前有 G 在执行,runtime 会将这个 MP 进行分离,如果有空闲的M就用或者是从线程池中取,如果没有就创建一个新的M 来服务于这个 P;
当 M 系统调用结束时候,这个 G 会尝试获取一个空闲的 P 执行,并放入到这个 P 的本地队列。如果获取不到 P,那么这个线程 M 变成休眠状态, 加入到空闲线程中,然后这个 G 会被放入全局队列中
4.5 拓展
判定阻塞的原理:
go程序启动时会首先创建一个特殊的内核线程 sysmon,用来监控和管理,其内部是一个循环:
记录所有 P 的 G 任务的计数 schedtick,schedtick会在每执行一个G任务后递增
如果检查到 schedtick 一直没有递增,说明这个 P 一直在执行同一个 G 任务,如果超过10ms,就在这个G任务的栈信息里面加一个 tag 标记
然后这个 G 任务在执行的时候,如果遇到非内联函数调用,就会检查一次这个标记,然后中断自己,把自己加到队列末尾,执行下一个G
如果没有遇到非内联函数 调用的话,那就会一直执行这个G任务,直到它自己结束;如果是个死循环,并且 GOMAXPROCS=1 的话。那么一直只会只有一个 P 与一个 M,且队列中的其他 G 不会被执行!
5. go func()调度流程
下方图片转自Go夜读 go
- 使用go关键子创建一个G,写法:
go func(){}
- 将G放入P的本地队列(如果当前M绑定的P的本地队列满了,会放在全局队列中)
- 唤醒或者新建M来执行任务
- 进入调度循环(M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去;)
- 尽力获取可执行的G,并执行(如果p本地队列没有可运行的G时,会去全局队列中拿取一半的,如果全局队列中也没有,则会进行执行work stealing机制,会随机的去另一个线程M中的P本地队列偷取一半的G来运行)
- 清理现场并重新进入调度循环
6. P和M的个数
6.1 P的数量
P的数量会由启动时环境变量$GOMAXPROCS 或是runtime的方法 GOMAXPROCS()来设定
6.2 M的数量
- go程序启动时默认的M的最大数量为 10000
- runtime/debug 中的 SetMaxThreads 函数,设置 M 的最大数量
- 当某个M阻塞了,会创建新得的M
6.3 P和M什么时候会被创建
- P:在确定了P的最大数量为n的时候,运行时系统会根据这个n创建n个P
- M:当的M都阻塞了,但是绑定的P中还有很多就绪任务G,这时会去寻找空闲的M或者去线程池中找,且找不到空闲的M的情况下会创建新的M
6.4 问题?
问题来自B战评论
整体的逻辑与单线程调度器没有太多区别,因为我们的程序中可能同时存在多个活跃线程,所以多线程调度器引入了 GOMAXPROCS 变量帮助我们灵活控制程序中的最大处理器数,即活跃线程数。
这个GOMAXPROCS 到底是 P的个数,还是M的个数呢?
答:GOMAXPROCS是限制P的个数,你可以理解成M是线程,P是M需要执行G的时候需要持有的局部资源,只有M持有P的时候才有局部资源可以执行G。注意,也存在M持有G但是不持有P的情况,这时候一般是由于M持有P执行G的时候陷入了长时间的系统调用,被系统监控sysmon发现后将P夺走,将P给另一个M用来继续执行其他G,被夺走P的M此时陷入系统调用,不使用CPU了,也不执行G。因此,总体上可以这么认为,M如果需要访问CPU资源,那么就需要持有P,同时有多少个CPU核心,那么就有多少个P,同时也就有多少个M可以使用CPU。
如果想要了解更多请看文档或源码:
G-P-M 模型的设计者的文档
Golang GMP调度模型详解相关推荐
- golang——GMP调度模型详解
目录 一.Golang调度器由来 存在问题: 3种协程和线程的关系 二.Golang对协程的处理 协程和goroutine关系 Go的GMP调度模型 P 和 M 何时会被创建 P和M的个数 调度器的设 ...
- Go面试必问——GMP调度模型详解
来源:http://www.topgoer.com/并发编程/GMP原理与调度.html 文章目录 GMP 原理与调度 Golang "调度器" 的由来? (1) 单进程时代不需要 ...
- Golang GMP调度模型
解释GMP模型含义 M结构是Machine,系统线程,它由操作系统管理,goroutine就是跑在M之上的:M是一个很大的结构,里面维护小对象内存cache(mcache).当前执行的goroutin ...
- golang之gmp调度模型
原始调度模型 我们把线程分为内核级线程和用户态线程,内核级的线程在切换线程时,开销比较大,需要系统调用,但是,用户态线程不是这样,用户态的线程之间的切换不需要系统调用,从而把切换的开销比较小. gol ...
- Golang知识点二、GMP调度模型
GMP调度模型 1. 调度器由来 调度器分为进程调度器和线程调度器. 1.1. 单进程时代 单进程系统存在一定问题:1. 单一执行流程.计算机只能一个任务一个任务处理 2. 进程阻塞所带来的C ...
- golang程序启动流程详解
golang程序启动流程详解 环境 go1.16.5 linux/amd64 用例 package mainimport "fmt"func main() {fmt.Println ...
- Go 语言编程 — 并发 — GMP 调度模型
目录 文章目录 目录 并发和并行 如何交互?CSP 通信模型 如何调度?GMP 调度模型 用户级线程模型(多对一) 内核级线程模型(一对一) 两级线程模型(多对多) GMP 线程模型 Go Runti ...
- 使用pickle保存机器学习模型详解及实战(pickle、joblib)
使用pickle保存机器学习模型详解及实战 pickle模块实现了用于序列化和反序列化Python对象结构的二进制协议. "Pickling"是将Python对象层次结构转换为字节 ...
- Transformer 模型详解
Transformer 是 Google 的团队在 2017 年提出的一种 NLP 经典模型,现在比较火热的 Bert 也是基于 Transformer.Transformer 模型使用了 Self- ...
最新文章
- 关于开始申请2010年4月份微软MVP的通知!
- C#的多线程机制探索4
- 在Windows IoT上使用网络摄像头
- 第2章:Maven的安装/2.2 Linux下的安装
- 列举在Web前端开发中经常会设置的特殊样式!
- LeetCode 451. 根据字符出现频率排序(map+优先队列)
- java 中for循环中重复定义的变量 为什么不报错?
- Malta中any函数
- 关于Oracle RAC调整网卡MTU值的问题
- Apache Commons DbUtils 入门
- Apple ID 被盗用的 5 个征兆,遇到其中一种,建议赶快改密码
- modelsim安装教程
- 2022最新软件设计师历年真题和答案解析分享!
- 单片机中断程序详解(转)
- 解线性方程 matlab,用matlab求线性方程的解
- 超声波传感器(CHx01) 学习笔记 Ⅵ - 原始数据
- @click.stop作用(阻止点击事件继续传播,即阻止事件冒泡)
- 面试-android
- SQL数据更新、视图
- 【香蕉oi】燃烧的火焰(最短路、状压)
热门文章
- PyCharm+PyQt5(5.15.2)+mysql, PyQt5连接mysql,踩坑与解决办法
- Python递归生成多叉树结构之treelib
- axios同步请求--
- 随机变量的分布及其数字特征
- java 小程序 公众号_如何玩转小程序+公众号?手把手教你JeeWx小程序CMS与公众号关联...
- GD32F103 中文芯片手册,用户手册
- python自动化导出数据库表结构到word
- 微信小程序 解决自定义顶部导航栏被键盘挤压的问题
- java优化上传速度慢怎么办_我是如何让minio client上传速度提高几十倍的
- 【MYSQL高级】Mysql的SQL性能分析【借助EXPLAIN分析】