浅谈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)。

相关推荐
蒙娜丽宁2 天前
Go语言错误处理详解
ios·golang·go·xcode·go1.19
qq_172805593 天前
GO Govaluate
开发语言·后端·golang·go
littleschemer3 天前
Go缓存系统
缓存·go·cache·bigcache
程序者王大川4 天前
【GO开发】MacOS上搭建GO的基础环境-Hello World
开发语言·后端·macos·golang·go
Grassto4 天前
Gitlab 中几种不同的认证机制(Access Tokens,SSH Keys,Deploy Tokens,Deploy Keys)
go·ssh·gitlab·ci
高兴的才哥5 天前
kubevpn 教程
kubernetes·go·开发工具·telepresence·bridge to k8s
少林码僧6 天前
sqlx1.3.4版本的问题
go
蒙娜丽宁6 天前
Go语言结构体和元组全面解析
开发语言·后端·golang·go
蒙娜丽宁6 天前
深入解析Go语言的类型方法、接口与反射
java·开发语言·golang·go
三里清风_6 天前
Docker概述
运维·docker·容器·go