写点什么

Golang 的 GMP:并发编程的艺术

  • 2023-09-22
    福建
  • 本文字数:2800 字

    阅读完需:约 9 分钟

前言

在 Golang 的并发编程中,GMP 是一个重要的概念,它代表了 Goroutine、M(线程)和 P(调度器)。这个强大的三位一体的并发模型使得 Golang 在处理并发任务时非常高效和灵活。通过 GMP 的组合,Golang 实现了一种高效的并发模型。它充分利用了多核处理器的优势,并通过轻量级的 Goroutine 实现了高并发的编程模式。但是 GPM 到底是怎么工作的呢?今天这篇文章就为您解开 GPM 的神秘面纱。

调度器的由来

单进程系统

图片


早期的计算机都是单进程操作系统,各个进程之间都是顺序执行,也就是进程 A 执行完了才能执行进程 B。


「对于 cpu 来说,进程和线程是一样的,这里我们就不讨论进程和线程的区别了」。

存在的问题
  • 单一执行流程,计算机只能一个任务一个任务的处理。

  • 如果进程 A 阻塞,会带来很多 cpu 浪费的时间。

多进程/线程操作系统


基于以上的问题,于是就出现了多进程/线程操作系统。


图片


  • 系统把 cpu 分成了一段一段的时间片(微妙级别)。

  • cpu 在第一个时间片执行进程 A,然后切换到进程 B 执行,再切换到进程 C,一直这样轮询的执行。

  • 因为 cpu 被分成的时间片是微妙级别的,所以直观的感觉就是进程 A,B,C 是在同时执行的。

  • 多进程/线程操作系统的确解决了阻塞的问题,但是又出现了新的问题。

存在的问题
  • 因为 cpu 需要不断地进程 A,B,C 之间切换,切换肯定避免不了各种复制,计算等消耗,所以在切换过程中浪费掉了很多时间成本,所以「进程/线程越多」,切换「成本就越大」,也就越「浪费」。

  • 在这种模式下运行 CPU 在切换动作上浪费的时间成本大概是 40%,只有 60%的时间是在执行程序。

图片
  • 进程和线程对内存的占用是比较大的,在 32 位的操作系统中,进程占用的虚拟内存大概是 4GB,现成占用内存大概是 4M。


图片

协程的诞生

对于一个线程来说其实分为两部分,「用户空间」和「内核空间」。

  • 内核空间主要是指操作系统底层,包括进程开辟,分配物理内存资源,磁盘资源等。

  • 用户空间主要是编码业务逻辑部分。

图片


  • 于是有人想到能不能把线程的内核空间和用户空间分开。并且让他们互相绑定在一起。


图片


  • 对于 cpu 来说,只需要关注内核空间的线程就可以了。


当然如果只是这样把用户空间的协程和内核空间的线程一一绑定还是没有解决问题的,如果开启的比较多,那么对应的线程也会跟着一起增加,cpu 频繁切换的问题还是没有解决,于是就引入了「调度器」的概念。

引入调度器来在各个协程之间切换,cpu 只需要关注内核空间的线程即可,这样「解决了 cpu 在各个协程之间不断切换的问题」。

存在的问题

这样设计虽然解决了 cpu 频繁切换的问题,但是如果协程 A 发生了阻塞,肯定会导致协程 B 无法被执行。而且如果计算机是多核,那么是无法利用到多核的优势的。显然是不合理的。


对于多核的计算机,在内核空间可以开启多个线程(具体开启几个由计算内核决定,人为无法控制),所以问题的核心点就转移到了协程调度器上面,不管是什么语言,「协程调度器」做的越好,相对的「cpu 利用率」也就越高。


图片

go 对协程的处理

内存控制和灵活调度

  • 首先 golang 对协程改名为 gorountine,并且把多余的空间都去掉,控制每个协程的内存在几 KB 大小,所以 golang 可以开启大量协程。

  • golang 对协程的调度非常灵活,可以经常在各个协程之间切换。


图片


go 对早期调度器的处理(GM 模型)


golang 在早起调度器处理是比较简单的,具体流程如下:


图片


  • 首先会有一个全局的 go 协程队列,并且加锁,防止资源竞争。

  • M 获取锁之后会去尝试执行 gorountine,执行完毕再把 gorountine 重新放回队列中。

GM 模型存在以下问题

  • 创建、销毁、调度 G 都需要每个 M 获取锁,这就形成了激烈的锁竞争。

  • M 转移 G 会造成延迟和额外的系统负载。

  • 系统调用(cpu 在 M 之间切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销。

  • 比如我再一个 G 中又开辟了一个 G1,那么 G1 和 G 当然在一个 M 上执行是比较合适的,因为存在一些共享内存,但是显然这种调度模式是无法做到的 基于以上问题,golang 针对这块做了一些改进,也就是我们今天的主角,GMP 模型。

GMP 模型

GMP 模型简介

GMP 模型主要指的是 G(gorountine 协程),M(thread 线程),P(processor 处理器)之间的关系。

全局队列

存放等待运行的 G。

P 的本地队列

  • 存放等待运行的 G。

  • P 的本地队列存放的 G 是有数量限制的,一般是不超过 256G。

  • 如果创建一个 G,是会优先放在 p 的本地队列中,如果满了则会放到全局队列中去。

P 列表

  • 在程序启动的过程时创建。

  • 最多有 GOMAXPROCS 个(可配置)。

  • 可以通过环境变量 $GOMAXPROCS 来设置 P 的个数,也可以在程序中通过 runtime.GOMAXPROCS()来设置。

M 列表

  • 当前操作系统分配到当前 go 程序的内核线程数。

  • go 语言本身,限制 M 的最大数量是 10000。

  • 可以通过 runtime/debug 包中的 setMaxThreads 来设置。

  • 如果有一个 M 阻塞,则会创建一个新的 M。

  • 如果有 M 空闲,那么会回收或者睡眠。

调度器的设计策略

复用线程

work stealing 机制


图片


  • M1 对应的 P 上面 G1 正在执行,G2 和 G3 处于等待中的状态。

  • M2 对应的 P 处于空闲状态。

这种情况下 M2 对应的 P 会从 M1 对应的 P 的本地队列中把 G3 偷取过来执行,提高 CPU 的利用率,这种机制叫做「work stealing 机制」。

hand off 机制


图片


如果 M1 和 M2 都在正常执行,但是 M1 对应的 G1 发生了阻塞,那么势必会影响到 G2 的执行,那么 GMP 是如何解决的呢?


图片


  • golang 会新创建一个 M3,用来接管之前的 P1 剩下的 G(G2)。

  • M1 和 G1 进行绑定再继续执行,执行完毕之后把 M1 设置为睡眠状态等待下一次被利用,或者直接销毁。

并行利用

并行利用其实比较好理解,其实也就是开启了多少个 P,P 的个数是有 GOMAXPROCS 来决定的,一般都会设置为 「CPU 核数/2」。

抢占策略

对于传统的 co-routine 来说,如果一个 C 和 cpu 进行了绑定,那么只有他主动释放,另外一个 C 才能和 cpu 进行绑定。但是在 golang 中,如果一个 G 和 cpu 进行了绑定,那么时间限制最多为 10ms,另外一个 G 就可以直接和 cpu 绑定。

抢占策略。

全局队列


图片


  • 全局队列的本质是对 work stealing 的一种补充。

  • 如上图,M2 对应的本地队列没有 G,会优先从 M1 的本地队列中偷取。

  • 如果 M1 的本地队列中也没有 G,那么就会从全局队列中去偷取 G3。

  • 因为全局队列涉及到加锁和解锁,所以效率相对要低一些。

go 的启动周期(M0 和 G0)

要想了解 go 的启动周期,首先得了解 M0 和 G0 的概念。

M0

  • 在一个进程中是唯一的。

  • 启动程序后编号为 0 的主线程。

  • 在全局变量 runtime.m0 中,不需要在 heap 上分配。

  • 负责初始化操作和启动第一个 G。

  • 启动第一个 G 之后,M0 就和其他的 M 一样了。

G0

  • 在一个线程中是唯一的。

  • 每次启动一个 M,都会第一个创建的 gorountine,就是 G0。

  • G0 仅仅用于负责调度其他 G,G0 不指向任何可执行的函数。

  • 每个 M 都会有一个自己的 G0。

  • 在调度或者系统调用的时候,会使用 M 切换到 G0 来调度。

  • M0 的 G0 会放在全局空间。

执行流程

package mainimport "fmt"
func main() { fmt.Println("Hello World")}
复制代码

比如我们看上断代码的执行流程。

初始化操作

在执行到 main 函数之前,会有一些初始化的操作,比如创建 M0,创建 G0 等等。


图片

执行具体函数

当执行 main 函数的时候,M0 已经和其他的 M 是一样的了,main 函数会进入 M0 对应的 p 的本地队列中,然后和 M0 绑定执行,如果执行超时(10ms),则会重新放到 M0 对应的本地队列中。一直到执行到 exit 或者 panic 为止。


图片


发布于: 刚刚阅读数: 5
用户头像

IT领域从业者 分享见解 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
Golang 的 GMP:并发编程的艺术_golang_树上有只程序猿_InfoQ写作社区