GMP模型
为什么G轻量
- 1、内核线程初始栈空间2M,协程初始栈空间2KB,自动扩容
- 2、内核需要频繁用户态内核态进行切换,切换成本较高
- 3、当协程阻塞时,调度器可以挂起协程,运行其他协程,提高利用率
- 4、线程间切换成本更高,协程间切换需要保存的运行现场更少
线程切换主要开销
- 切换内核栈
- 切换硬件上下文
- 保存寄存器中的内容
- cpu告诉缓存失效
为什么需要P
- 全局runq保存goroutine协程,造成对锁依赖严重,效率低
- M创建的G被放到其他M上运行,破坏了局部性原则,造成不必要的系统开销
- 每个M都需要分配内存,而真正执行中的M仅占1%
- 存在系统调用时,M会被阻塞,无法执行其他任务,造成系统资源浪费
- P能实现work stealing 机制和hand off
G的状态
1、Gidle 2、Grunnable 3、running 4、syscall 5、waiting 6、dead 7、copystack 8、preempted
GPM模型
- goroutine来自协程的概念,让一组可复用的函数运行在一组线程之上,一个goroutine只占几KB,所以可以灵活调度,高并发
- 如果只有G和M的话,M想要执行G都必须要访问全局G队列,并且M有多个,即多线程访问同一资源需要加锁进行保证互 斥/同步,所以全局G队列是有互斥锁进行保护的。 这就形成了激烈的锁竞争
只后引进了P,如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G队列
全局队列(Global Queue):存放等待运行的G。
- P的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建 G’时,G’优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。
- P列表:所有的P都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS (可配置)个。
- M:线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一 批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。M运行G,G执行之 后,M会从P获取下一个G,不断重复下去。
- Go语言调度器使用了一种策略:P 中每执行61次调度,就需要优先从全局队列中获取一个G到当前P中, 并执行下一个要执行的G
没有足够的M来关联P并运行其中的可运行的G。比如所有的M此时都阻塞住了,而P中还 有很多就绪任务,就会去寻找空闲的M,而没有空闲的,就会去创建新的M。
hand off机制
- Go语言的 hand off 机制 和 work steaing 机制一样,也是一种用于调度协程(Goroutines)的策略,有助于充分利用多核 CPU,提高并发性能,减少线程空转,从而使 Go 程序更高效地运行 核心思想是当线程 M 因为 G 进行的系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的 M 执行
- Go scheduler 可以说是 Go 运行时的一个最重要的 部分了。 Runtime 维护所有的 goroutine ,并通过 scheduler 来进行调度。
goroutine什么时候会被挂起
- 发生阻塞,例如等待 I/O 操作的完成或者发送或接收通道上的数据时没有可用的对等方。
- 发生调用 runtime.Gosched(),让出 CPU 给其他 goroutine 执行。
- 发生同步操作,例如 sync.Mutex 或 sync.WaitGroup 的锁定和解锁操作。
- 发生垃圾回收(GC)。
- 发生错误,例如 panic 或者超时
在以下情形下,会切换正在执行的goroutine
- 抢占式调度 sysmon 检测到协程运行过久(比如sleep,死循环) 切换到g0,进入调度循环
- 主动调度 新起一个协程和协程执行完毕 触发调度循环 主动调用runtime.Gosched() 切换到g0,进入调度循环 垃圾回收之后 stw之后,会重新选择g开始执行
- 被动调度 系统调用(比如文件IO)阻塞(同步) 阻塞G和M,P与M分离,将P交给其它M绑定,其它M执行P的剩余G 网络IO调用阻塞(异步) 阻塞G,G移动到NetPoller,M执行P的剩余G atomic/mutex/channel等阻塞(异步) 阻塞G,G移动到channel的等待队列中,M执行P的剩余G