写点什么

golang-GMP 模型

作者:
  • 2023-04-13
    日本
  • 本文字数:1337 字

    阅读完需:约 4 分钟

golang-GMP模型

Goroutine

首先协程按照我的理解一直是用户级线程,说白了就是它是 go 自己构造的,当前线程运行的时候例如有 10ms 的 cpu 时间,那么可以 5 个 go 的协程每个分到 2ms。

说点官方的话就是 进程分配内存空间,cpu 的工作是线程来分配的,但是 cpu 切换线程的时候上下文切换虽然不及进程但是开销也是较大的,所以出现了协程,它运行在线程上,上下文切换的开销更小也更轻量。


goroutine 的栈大小

我模拟一段代码

func main() {	for i := 0; i < 200000; i++ {		go func() {			time.Sleep(1000 * time.Second)		}()	}
time.Sleep(1000 * time.Second)}
复制代码


我启动 20w 个协程看看内存

执行 ps -ef | grep go 获得进程号

然后 htop -p {进程号}


RES 列为使用的物理内存单位是 byte,粗略计算下,20W 个协程占用内存大小为 19mb(也太少了)


GMP 模型

GMP 模型中的 G 全称为 Goroutine 协程M 全称为 Machine 内核级线程P 全称为 Processor 本地队列。

首先在我个人学习的时候知道是从 GM 过度到的 GMP 模型的,在没有 P 的情况下,可以理解成这个模型(网上找的图)

可以发现有一个全局队列,那么既然是全局队列在并发的情况下就会涉及到锁,每个线程想要获取 G 的时候必然是要访问的。那么锁竞争激励的话那么效率就会变低。

如何解决这个情况?

引入了一个 P,P 有一个本地队列,那么当 M 和 P 绑定的时候如果需要获取 G 可以直接从本地队列获取,那么有效的解决了锁竞争的问题。还有一个处理的点就是,如果本地队列的 G 已经没有的话依旧是从全局队列获取,但是 go 的设定不是每次只拿一个,在获取到锁之后会一次性拿很多,这样也算是优化了频繁获取锁。



最后在源码中其实也能看到(当然不是看具体代码,看注释)

stealWork attempts to steal a runnable goroutine or timer from any P.

它尝试去偷可运行的 goroutine 在任何 P 当中,也就是说如果本地没有了,全局没有了,但是其他 P 有!那么我们就去偷而不是摸鱼。


学到这里开始思考下一个问题,如果我在程序运行的时候创建了一个 go 协程,那么它是存放在本地队列还是会放在全局队列,是放在队尾还是可以插队。在最后查阅资料过后得到了结论是:随机寻找一个本地队列,然后插队,但是如果本地队列满了的话依然是放到全局队里,代码就不读了理解个思想就行了,和我设想的差不多。


基于协作式的抢占调度

假设一个 demo

func Do() {	i := 0	for true {		i++	}}func main() {	go Do()	select {}}
复制代码

协作式调度依靠被调度方主动弃权,其实在写协程的业务代码的时候就得考虑清楚什么时候放弃当前时间片移交给下一个协程,所以如果写出上面的代码在整个 for 循环当中没有切换的步骤的话,那么当前线程应该是就被这个协程吃死了。


  • 同步协作式调度主动用户让权:通过 runtime.Gosched 调用主动让出执行机会;

  • 主动调度弃权:当发生执行栈分段时,检查自身的抢占标记,决定是否继续执行;


可以看看如下过程


这是如上代码的汇编代码,可以发现有一个 runtime.morestack 顾名思义就是检查调用栈的空间,所以在业务的场景中会经常出现这个方法,所以可以考虑埋一个钩子来标记抢占。可以看看如下源码



如果 stackguard0 被标记成了如下 16 进制,那么其实就表示需要 schedule 了,不过如果用户写出上述死循环,出现不了 morestack 的话怎么办?所以出现


基于信号抢占式的调度

注册一个 SIGURG 的信号,在 GC 的时候清理.


发布于: 刚刚阅读数: 5
用户头像

关注

爱技术,记录学习过程 分享分享生活 2021-03-24 加入

喜欢数据结构与操作系统

评论

发布
暂无评论
golang-GMP模型_友_InfoQ写作社区