0
点赞
收藏
分享

微信扫一扫

Golang高并发的“灵魂”——GMP模型

导语

基础概念介绍

进程

通常表示计算机中正在运行的程序实例。

关键点:

  1. 进程包含代码段、数据段和堆栈
  2. 多个进程可以同时运行,相互隔离
  3. 每个进程独占计算机CPU和内存等资源,是获取资源的基本单位

线程

通常表示内核级的线程。计算机中最小可执行单元

关键点:

  1. 计算机中最小可调度、可执行单元
  2. 不独立拥有系统资源,共享进程内资源
  3. 可以充分利用多核进行并发、并行执行
  4. 创建、调度、销毁都需要CPU参与,实现用户态和内核态切换

协程

通常也被称为用户级线程,只存在于用户空间

协程与线程对应关系图.png 关键点:

  1. 与线程是N:1(多对一)的关系
  2. 创建、调度、销毁都是在用户空间完成的,无需CPU参与,更轻量
  3. 都映射到同一个线程,无法实现并行,如果一个协程阻塞,则整组协程全部阻塞

Goroutine

经过Golang优化后的“协程”

Goroutine和线程关系图.png 关键点:

  1. 与线程是M:N(多对多)的关系
  2. 创建、调度、销毁都是在用户空间完成,对内核透明,更轻量
  3. 映射至多个线程,可实现并行
  4. 经过调度器(processor)的调度,能实现动态绑定和灵活调度
  5. 栈空间可动态扩缩容

GM模型

在介绍GMP模型之前,先看看go1.1版本之前采用的调度模型——GM模型

GM = Goroutine + Machine

G指的就是Goroutine协程,M指的就是Machine内核级线程 GM调度模型见下图 GM模型图.png

从上图中可以看到,每个线程M在获取Goroutine时,都需要从全局队列中获取,这时候就必须要先获取锁,然后才能获取Goroutine,而如果被M获取的Goroutine在运行过程中产生了新的Goroutine,则新的Goroutine也会被放回到全局队列中。通过描述,我们很容易发现GM模型有这么几个缺点:

  1. 调度:获取和返回Goroutine都需要先获取全局队列的锁,这就会在获取锁这里形成激烈地竞争,严重影响性能
  2. 资源利用率:当Goroutine1结合M1产生新的Goroutine2时,由于M1要继续运行Goroutine1,那么就需要把Goroutine2交还到全局队列,然后由其他的M获取后运行,而Goroutine1和Goroutine2是有相关性的,上下文都保存在M1中,这样会导致上下文切换或者保存原有状态是产生不必要的系统开销。
  3. 缓存资源:每个M在获取到Goroutine时,都需要缓存Goroutine的状态、加载上下文,这就要求M必须要配有一定的存储。而这就会产生严重的问题,我们知道M获取的Goroutine如果产生阻塞,M就会被悬挂起来,当M不够用时,系统会产生新的M,而每产生一个新的M就要对应地分配存储资源,这就增加了存储系统的开销,极端一点可能会导致存储资源消耗殆尽(一般系统中M存在的数量上限是10000),这显然是不合理的。

鉴于GM模型的上述几个缺点,在go1.1之后的版本,引入了P(processor)形成GMP模型来解决GM模型的几个缺点问题

GMP = Goroutine + Machine + Processor

GMP模型.png

解释一下上图的调度过程

  1. 当我们使用go func()创建一个G时,首先会被加入到一个P的本地队列中,以供P进行调度。
  2. 当P获取到一个G之后,会从空闲M队列中获取一个M(如果没有则会新建一个M),此时P同时获取到G和M,G只能跟M绑定运行,这样就可以正式运行G任务了。
  3. 在运行G的过程中,如果产生了新的G,则会将新产生的G放入到P的本地队列中,如果P的本地队列已满,则会放入全局队列中。
  4. 当一个G运行结束后(未发生系统调用),P会从本地队列中获取下一个G来运行,如此往复,直到P的本地队列为空。此时P首先会查找其他P的本地队列,如果其他P的本地队列中还有尚未执行的G,则会“偷取”一半数量的G,放到自己的本地队列中继续执行;而如果其他P的本地队列中也没有多余的G,则会从全局队列中获取G来执行。
  5. 而如果跟P绑定的M在运行G的过程中发生了系统调用(如I/O),P会跟绑定的GM解绑,解绑后GM会去处理系统调用,而P会重新获取一个G和一个M运行。当原来的GM处理完成后,会重新获取原来的P,如果原来的P已被占用,则会从空闲P队列中获取一个P,如果还没有,则G会被添加到全局队列中,M会被添加到空闲M队列中。
举报

相关推荐

0 条评论