GMP模型
在学习GMP模型过程中,看了网上许多GMP模型相关的文章,要么缺少背景的介绍、要么陷入无限的细节中,让我们初学者很难建立整体的认识,很难将这块知识串起来,当然就会看的云里雾里。
本文希望帮助初学者建立知识整体脉络,让大家在宏观上能有一个全局的认识,希望对大家有所帮助。
这块内容参考了刘丹冰老师的文章,在B站也对应的视频,讲的非常详细,在此感谢刘老师的无私奉献。
当然本文除了参考上面文章外,也添加了许多其它的认识,您可以把本文当作一个学习向导,好啦,让我们开始吧!
1. 已经有了进程、线程为啥还需协程?
我们知道在计算中有进程和线程,我们先简单回顾下:
-
进程:
- 它是一个程序运行的实例
- 有独立的内存空间和系统资源
- 可以简单理解为它是线程的容器,内部有多个线程
-
线程:
- 它是是进程的实例
- 共享进程中的内存空间和系统资源
- 操作系统运算调度的最小单元
我们可以这样简单的理解,进程先向操作系统申请一块资源,线程利用这些资源执行,线程的执行是通过操作系统cpu上调度运行的。因此在操作系统中,真正执行程序的实际是线程;
在早期的编程语言中,我们想实现程序的并发能力,沿用操作系统的进程、线程方式,主要采用多线程实现,但是随着时间的推移,我们慢慢发现了多线程的许多弊端:
- 内存占用高(一个线程大概几M)
- 切换成本高(内核态下切换)
因此,后来的编程语言设计者希望采用一种轻量、切换成本低的方式来实现并发 ,但是在操作系统下的进程、线程模型已经没有什么文章可以做了;因此他们想到了既然内核态下不能做什么文章,那么在用户态下,我们能不能实现一种类似线程、但又不同于线程、非常轻量、可以并发或者并行,由编程语言自己管理的东东呢?
对滴,它就是协程(coroutine) 因此许多编程语言都实现了自己的协程,只是在叫法上存在差异,有的语言中又叫做纤程,在go中叫goroutine。
在go中由于一个goroutine非常小只有几KB,因此我们可以轻而易举的创建上万个协程。
2. 协程调度器
协程本质上还是在操作系统线程执行的,对操作系统而言,它只知道有进程和线程,并不清楚有协程。
协程的管理由编程语言自己实现,这里就牵扯到一个问题------该如何管理这些协程?协程和线程之间是如何关联起来的等等?
这就引入了一个概念 ------ 协程调度器。
一门编程语言它的协程设计的好好不好,比拼的其实就是调度器设计好不好。当然go也不例外,也需要设计实现自己的调度器。
3. 第一阶段GM模型
go的调度器设计,主要有两个阶段,在第一阶段是没有P (它在第二阶段,下面会说),只有GM:
- G: 指的是Goroutine(协程)
- M: 指的的是线程
第一阶段比较简陋,大概这样: 每个M(线程),都去全局队列中取G来执行,为了保证同一个G只能被一个M取走,每次都需要对全局队列加锁。
缺点: 是获取锁竞争非常大,性能不好。
4. 第二阶段模型-GMP
为了解决第一阶段锁竞争大的问题,引入了P(processor处理器),每个P都会有一个存放了G的本地队列,可以从本地队列直接获取G,而不用加锁。
4.1 整体认识
在这一阶段GMP也凑齐啦,这里涉及到许多细节,如果分开一个个去看非常容易陷入到只见树木不见森林的境地;
其实我们只需要能理解go func()
从创建到执行的整个流程,建立起整体框架认识,后面理解具体的细节就会轻松很多,我们一起来看下面的图吧。
下面简述下:
go func()
创建一个G(goroutine)- G队列
- 如果本地队列未慢,则优先加入到本地队列
- 如果本地队列已满,则将他加入到全局队列
- M需要获取G来执行
- 如果P的本地队列中有,则从本地队列获取
- 如果P的本地对立中没有,则从全局对立中获取
- 如果全局队列中也没有,则从其它P中偷取(work stealing)
- 调度执行
- 如果正常执行完,则销毁G,开启下一轮的M获取G,依次循环
- 如果执行过程中发生系统调用/阻塞,则当前M和P解除绑定(hand off)机制,让出P;P会寻找是否有休眠中的M,如果有,则与之绑定,如果没有则创建一个M与之绑定
补充知识点:
- 一个M要执行必须先获取到P
- 从上图我们可以轻易的分析出G和M之间的关系是M:N
- P的个数可以通过GOMAXPROCES设置,默认情况下为cpu核心数
4.2 调度器生命周期
前面我们看了GMP模型的流转过程,但是忽略整个过程中两个特殊的对象M0
和G0
- M0: go进程创建的第一个线程,全局唯一
- G0: 每创建一个M都会创建一个G0,它的作用主要在于在M上调度goroutinue,用户goroutine的切换需要通过G0来操作
假设我们运行一段main.go
整个过程是怎样的呢?
- 程序创建第一个线程M0
- 由于每个M都需要创建一个G0,因此这个时候会创建一个M0的G0
- 创建main.main的goroutine,加入本地队列
- M0和P绑定,从本地中获取到G(main的goroutine)
- 执行main的goroutine
图示大概为:
4.3 一些场景
在GMP中存在许多细化场景的分析,由于场景太多这里就不一一列举了,敢兴趣可以到B站这里来看具体某个场景下的处理流程。
5. 总结
GMP模式是一种经过go设计者不断优化而形成的一种合理方案,在go中我们可以认为只有协程,没有线程。因为我们从main的启动来看,启动的其实也是一个goroutine;
go能实现很高并发的原因在于,通过GMP模型可以只利用很少的线程(复用线程),在不切换线程的情况下可以非常容易的切换/运行大量goroutine)。