浅谈golang GMP模型

GMP模型

在学习GMP模型过程中,看了网上许多GMP模型相关的文章,要么缺少背景的介绍、要么陷入无限的细节中,让我们初学者很难建立整体的认识,很难将这块知识串起来,当然就会看的云里雾里。

本文希望帮助初学者建立知识整体脉络,让大家在宏观上能有一个全局的认识,希望对大家有所帮助。

这块内容参考了刘丹冰老师的文章,在B站也对应的视频,讲的非常详细,在此感谢刘老师的无私奉献。

当然本文除了参考上面文章外,也添加了许多其它的认识,您可以把本文当作一个学习向导,好啦,让我们开始吧!

1. 已经有了进程、线程为啥还需协程?

我们知道在计算中有进程和线程,我们先简单回顾下:

  • 进程:

    1. 它是一个程序运行的实例
    2. 有独立的内存空间和系统资源
    3. 可以简单理解为它是线程的容器,内部有多个线程
  • 线程:

    1. 它是是进程的实例
    2. 共享进程中的内存空间和系统资源
    3. 操作系统运算调度的最小单元

我们可以这样简单的理解,进程先向操作系统申请一块资源,线程利用这些资源执行,线程的执行是通过操作系统cpu上调度运行的。因此在操作系统中,真正执行程序的实际是线程;

在早期的编程语言中,我们想实现程序的并发能力,沿用操作系统的进程、线程方式,主要采用多线程实现,但是随着时间的推移,我们慢慢发现了多线程的许多弊端:

  1. 内存占用高(一个线程大概几M)
  2. 切换成本高(内核态下切换)

因此,后来的编程语言设计者希望采用一种轻量、切换成本低的方式来实现并发 ,但是在操作系统下的进程、线程模型已经没有什么文章可以做了;因此他们想到了既然内核态下不能做什么文章,那么在用户态下,我们能不能实现一种类似线程、但又不同于线程、非常轻量、可以并发或者并行,由编程语言自己管理的东东呢?

对滴,它就是协程(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()从创建到执行的整个流程,建立起整体框架认识,后面理解具体的细节就会轻松很多,我们一起来看下面的图吧。

下面简述下:

  1. go func()创建一个G(goroutine)
  2. G队列
    • 如果本地队列未慢,则优先加入到本地队列
    • 如果本地队列已满,则将他加入到全局队列
  3. M需要获取G来执行
    • 如果P的本地队列中有,则从本地队列获取
    • 如果P的本地对立中没有,则从全局对立中获取
    • 如果全局队列中也没有,则从其它P中偷取(work stealing)
  4. 调度执行
    • 如果正常执行完,则销毁G,开启下一轮的M获取G,依次循环
    • 如果执行过程中发生系统调用/阻塞,则当前M和P解除绑定(hand off)机制,让出P;P会寻找是否有休眠中的M,如果有,则与之绑定,如果没有则创建一个M与之绑定

补充知识点:

  • 一个M要执行必须先获取到P
  • 从上图我们可以轻易的分析出G和M之间的关系是M:N
  • P的个数可以通过GOMAXPROCES设置,默认情况下为cpu核心数
4.2 调度器生命周期

前面我们看了GMP模型的流转过程,但是忽略整个过程中两个特殊的对象M0G0

  • M0: go进程创建的第一个线程,全局唯一
  • G0: 每创建一个M都会创建一个G0,它的作用主要在于在M上调度goroutinue,用户goroutine的切换需要通过G0来操作

假设我们运行一段main.go整个过程是怎样的呢?

  1. 程序创建第一个线程M0
  2. 由于每个M都需要创建一个G0,因此这个时候会创建一个M0的G0
  3. 创建main.main的goroutine,加入本地队列
  4. M0和P绑定,从本地中获取到G(main的goroutine)
  5. 执行main的goroutine

图示大概为:

4.3 一些场景

在GMP中存在许多细化场景的分析,由于场景太多这里就不一一列举了,敢兴趣可以到B站这里来看具体某个场景下的处理流程。

5. 总结

GMP模式是一种经过go设计者不断优化而形成的一种合理方案,在go中我们可以认为只有协程,没有线程。因为我们从main的启动来看,启动的其实也是一个goroutine;

go能实现很高并发的原因在于,通过GMP模型可以只利用很少的线程(复用线程),在不切换线程的情况下可以非常容易的切换/运行大量goroutine)。

相关推荐
郝同学的测开笔记18 小时前
云原生探索系列(十二):Go 语言接口详解
后端·云原生·go
一点一木1 天前
WebAssembly:Go 如何优化前端性能
前端·go·webassembly
千羽的编程时光2 天前
【CloudWeGo】字节跳动 Golang 微服务框架 Hertz 集成 Gorm-Gen 实战
go
27669582923 天前
阿里1688 阿里滑块 231滑块 x5sec分析
java·python·go·验证码·1688·阿里滑块·231滑块
Moment4 天前
在 NodeJs 中如何通过子进程与 Golang 进行 IPC 通信 🙄🙄🙄
前端·后端·go
唐僧洗头爱飘柔95275 天前
(Go基础)变量与常量?字面量与变量的较量!
开发语言·后端·golang·go·go语言初上手
黑心萝卜三条杠5 天前
【Go语言】深入理解Go语言:并发、内存管理和垃圾回收
google·程序员·go
不喝水的鱼儿5 天前
【LuatOS】基于WebSocket的同步请求框架
网络·websocket·网络协议·go·luatos·lua5.4
微刻时光5 天前
程序员开发速查表
java·开发语言·python·docker·go·php·编程语言
lidenger6 天前
服务认证-来者何人
后端·go