GMP调度模型入门教程

本文深入探讨了Go语言中的GMP模型,详细阐述了从早期的goroutine调度器到现代GMP模型的演进,包括1:1、N:1和M:N的协程线程绑定方式。Go的GMP模型通过引入Processor(P)角色,减少了锁竞争,优化了goroutine的调度。文章还提到了m0和g0的特殊性以及自旋线程的概念,最后通过实例展示了GMP的工作流程。
摘要由CSDN通过智能技术生成

本文主体分为2大部分,先介绍了GMP的由来,再介绍了GMP的调度模型,剖析了GMP的工作过程,适合初学GMP的读者。

GMP调度器的由来

单进程操作系统不需要调度器

单进程操作系统中每个进程是串行运行的,没有并行的概念,也就是当进程A阻塞时,CPU也不会切换到其他进程,会一直等待A解阻塞。
在这里插入图片描述

多进程/线程操作系统有了调度器需求

在多进程/线程的操作系统中,如果一个进程/线程发生阻塞那么CPU就会切换到另一个进程/线程运行,这样从宏观上来看多个进程/线程就绪并行的。那此时就引入额外的问题,如果有过多的进程/线程的话,CPU会花很多时间在创建、销毁、切换上,这样就会导致CPU利用率很低。
在这里插入图片描述

协程的出现

多进程/线程虽然提高了系统的并发能力,但是一个进程/线程也占用了较多的内存(32位操作系统进程虚拟地址空间4GB,线程大约8MB),所以大量的进程/线程会带来以下2个问题:

  1. 高内存的占用
  2. 调度消耗大量CPU时间片
    但是上面说的线程是内核线程,而程序直接管理的是处于用户态的线程 — 用户线程,也成为co-routine(协程),但是CPU调度的是内核线程,它感知不到用户线程的存在,所以用户线程需要调度器去调度。
    在这里插入图片描述

通过看图可以看到每个用户线程都会绑定到某个内核线程,那它们的绑定关系是怎么样呢?

  • 1:1
    协程和线程为一一对应的关系,如下图所示。但这种情况下协程切换还是会涉及到内核线程的切换,还是会出现上面的问题。
    在这里插入图片描述
  • N:1
    这种情况下多个协程绑定到同一个内核线程,如下图所示。这种情况下协程的切换仅仅在用户态就可以完成,不需要内核的参与。当然,这种情况也有缺点,例如一个协程进行系统调用导致线程阻塞,那么其他协程也就一直得不到执行,也就没有并发可言了。
    在这里插入图片描述
  • M:N
    这种情况下和以上2种情况不一样,每个协程没有强制关联上某个线程,它们的关联是可变的,如下图所示。该模式下,协程的切换也是在用户态中实现的,不需要CPU的参与,每个协程阻塞后会执行下一个协程。
    在这里插入图片描述

Go的协程

Go语言为了更易用、高效的并发能力,提出了goroutine的概念。通常情况下,一个goroutine只占用几kb的内存,远低于内核线程(几M),而且goroutine的整个生命周期都是由runtime包进行管理,不需要内核的参与。

剖析GMP模型

通过上文我们可以看到协程的调度都是在用户态实现的,不需要内核的参与。那么在Go中协程的调度器是怎么实现的呢?

早期的goroutine调度器

在这里插入图片描述
上图为早期的goroutine调度器,它的主要思想就是用一个队列维护所有的goroutine,创建、销毁、调度goroutine都需要先获取一把全局锁保证多线程下资源互斥。
这种调度器有明显的几个缺点:

  1. 每次创建、销毁、调度goroutine都需要争抢全局锁,带来了激烈的锁竞争,降低了并发度。
  2. 工作线程经常传递goroutine,这可能会带来延迟的増加和额外开销。例如:M0目前执行G,G由创建了G‘,那这时M0会把G’交给其他线程处理,会造成很差的局部性,因为G和G‘关联,最好都放在M0上执行。
  3. 系统调用导致线程阻塞/解阻塞,这会带来大量的系统开销。

现在的goroutine调度器

在这里插入图片描述
目前的goroutine调度模型也成为GMP(goroutine、thread、processor)模型,相比于旧版调度器加入了processor的角色,每个M想要运行G必须要绑定某个P,其中P和M的关系是灵活的,每个P可以绑定任何一个M。

  • 每个P有本地G队列,大小为256,如果满了就会放到全局队列中
  • 当P被某个M执行时,首先会调度本地队列中的G,如果本地队列为空则从全局队列转移一部分G过来,如果全局队列也为空则会从其他P窃取
  • P的个数为GOMAXPROCS(可以通过runtime.GOMAXPROCS()设置),在程序启动时就会创建好P
  • 如果调度G时发生系统调用阻塞,则会解绑该G和P的关系,从而这个P又可以被其他M运行

特殊的m0、g0
go语言中存在特殊的M(m0)、特殊的G(g0),m0可以理解为go程序启动创建的第一个M,也就是main M(主线程),它负责创建第一个G,之后就和其他M没有区别。而每个M都会绑定一个g0,这个g0的栈是分配在内核空间,它主要的功能就是调度其他G。

自旋线程
如果m绑定的p本地队列为空,那这个m会不断寻找可运行的g,这种状态下的m称为自旋线程,自旋线程最大个数为GOMAXPROCS,也就是p的个数。

我们先通过下面这个例子对gmp有个宏观的认识。

go func(){xxxxxx}

上面这个代码就是起了一个goroutine来运行代码,下面这个流程图展示了gmp的工作流程。

参考文档

  1. Google官方 Go GMP设计文档
  2. Go GMP详解
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值