golang GMP 调度模型详解
为什么要有 goroutine ?
goroutine 是基于线程之上封装的用户协程, 但是在此之前一个进程已经可以创建多个线程来并发执行任务了
关于线程有个有意思的问题
有了进程,为什么还要有线程?
-
空间开销: C语言中一个空转的进程的占用是 6MB 左右, 线程看用户自己设置, 一般 32 位是 4MB, 我的机器 Linux 开发机64位是 10MB这个可以自己设置, 所以空间开销影响不大
-
切换时间开销: 进程和线程的内核栈和上下文的时间差距不大, 但是进程会更换虚拟地址空间导致虚拟地址空间的缓存 TLB Cache 失效拖慢整个程序后面的运行
-
通信开销: 进程间通信的成本至少是进程类通信成本的几个数量级, 很多时候进程内通信只需要传递一个指针
有了线程为什么要有 goroutine?
goroutine 是基于线程上面封装的用户协程, 理解为一个线程执行着一个队列中的任务
- 空间开销: 每个线程一般要占用 4 M以上的内存, gorotine 只需要 4K, 所以让大量的 goroutine 成为了可能
- 切换时间开销: 切换时间: 线程的切换会反复向操作系统申请创建和销毁资源来恢复CPU寄存器的状态, 一般线程的切换需要 1us 左右; goroutine 只需要 0.2 us 左右, 能节约 80% 以上的上下文切换时间
下面介绍 goroutine 的调度模型 GMP
GPM 模型
GMP 结构
- G: goroutine 协程(G0 在P队列中唯一)
- M: 线程(M0 进程全局唯一)
- P: Processor 调度器
P 的本地 G 队列
G 全局队列
M 休眠队列
P 队列
自旋线程
创建 goroutine
新建的 G 优先本地队列, 本地满了将队列的前面一半打乱和新建的 G 一起放入全局队列中
切换 goroutine
本地 -> 全局 -> steal , 切换的过程需要 G0 作为中间商
全局获取 G 和 steal 不是获取一个 而是批量获取
当队列中没有除了 G0 之外的 G 的 GMP 组合叫做自旋线程, 自旋线程的数量会收到 P 的 Max 数量限制
自旋线程会优先 全局 -> steal
全局是批量获取, 负载均衡算法, 保证给其他的P一点机会留点
偷取一般是偷取一半
系统阻塞
- 系统调用发生阻塞,为了不耽误P中其他G的调用, 此时 M 继续于发生系统调用的 G 绑定执行, P 在休眠的线程中找到一个新的 M (没有就新建) 然后绑定到新的 M 上面执行, 原来阻塞的 G 还是绑定到原来的 G 上执行,
- 阻塞结束后, M会想要执行绑定的原来的 G, 但是此时没有 P 是无法执行 G 的, 所以 M 就回去找 P , 但是此时原来的 P 已经更新的 M 绑定了, M 抢占原来的 P 会失败, M 就会尝试去空闲的 P 队列中找到新的 P 执行, 找到就绑定执行, 空闲 P 队列中没有 P 就绑定失败, G 进入全局 G 队列中, M 休眠进入休眠 M 队列中.
GMP抢占式调度
对比操作系统的进程调度
操作系统的进程/线程中断
-
系统调用:
-
trap:
-
时间片(n*时钟周期)
时间片就能解决一些边界问题, 比如一个 for 的空转, 不系统调用也不trap, 一个线程就能恶意一直抢占CPU
我理解的 GMP 就是原有线程上面封装协程, 屏蔽的底层的这些复杂的中断机制(一般讲 go gmp 的书上也不会跟操作系统的调度一起讲解, 一般都是理解 go 协程一直在线程上执行就行了)没有时钟周期的中断, 就可能存在一些恶意抢占的程序, go 1.14 之前的基于协作式的抢占调度就可能出现这个问题, 之前也有很多关于 for 空转的 issue 提交到 GitHub 的 go 代码库上面, 直到 go 1.14 之后更新基于 single 信号量的抢占式调度才解决这个问题
golang 1.14 之前的协作式抢占调度
-
编译器在调用函数前插入一个是否抢占的检查 runtime.morestack
-
gc 前 scan stack 的时候 短暂 stw, 标记超过 10ms 的 goroutine 抢占变量为可以抢占
-
下次 goroutine 进入函数的时候执行是否抢占的检查, 如果抢占变量为可以抢占, goroutine 就退出
问题:
如果 goroutine 空转, 比如就一个 for 计算, 不调用任何函数, 及时在 gc 时候标记了该 goroutine 需要退出也无法退出
golang 1.4 之后的基于信号的抢占调度
- 程序启动的时候 M0 线程全局注册 sigurg (si gu r g)信号处理的函数
- gc scan stack 的时候短暂的的 stw, 给运行大于 10ms 的 G 的线程发送信号 sigurg, 操作系统 会中断改线程并执行注册的函数完成抢占
这次是基于操作系统的信号处理, 操作系统有时钟周期兜底, 所以能解决上面的恶意程序空转一直抢占的问题
refrence
Linux中进程和线程的开销基本一样啊,为什么还要多线程呢? - 知乎 (zhihu.com)
请你说一说有了进程,为什么还要有线程?牛客网 (nowcoder.com)
一个线程占用多少内存
linux栈空间大小
进程切换开销大的原因_m0_49036370的博客-CSDN博客_为什么进程切换的开销比线程大