0
点赞
收藏
分享

微信扫一扫

[面试]golang GMP 调度模型详解

北邮郭大宝 2022-02-16 阅读 135

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 结构

image-20220203210907114

  • 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博客_为什么进程切换的开销比线程大

举报

相关推荐

0 条评论