2

GMP调度模型

 2 years ago
source link: https://helloteemo.github.io/2021/07/09/Golang/%E5%9F%BA%E7%A1%80/GMP%E8%B0%83%E5%BA%A6%E6%A8%A1%E5%9E%8B/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

GMP调度模型

发表于 2021-07-09 更新于 2021-09-20 分类于 Golang基础 阅读次数: 28 本文字数: 3.4k 阅读时长 ≈ 3 分钟

开坑,今天太晚明天写


2021-07-12 09:14

这就是假期后遗症和懒。。。。。根本腾不出手来写东西

GMP调度模型

这里还是不介绍并发、并行的区别了,我们先从多线程到多协程的转化开始吧。

现在绝大部分的开发语言都支持线程级别的并发,最典型的就是 Java 了,多线程尽管一定程度上提高了并发能力,但是在现如今高并发的场景,为每一个任务创建一个线程是不现实的,因为会消耗大量的资源(在32位操作系统中进程虚拟空间会占用4GB,线程会占用大约4MB的空间)。而且大量的线程会出现的问题:高内存占用、调度的高CPU消耗,高内存占用我们可以使用线程池技术来进行缓解,但是高CPU调度问题我们就得另寻他法了。

我们重新认识一下线程,一个线程实际分为了用户态线程和内核态线程,一个用户态线程必须绑定一个内核态线程,但是CPU不清楚用户态线程的存在,它只知道运行的是一个内核态的线程。这里我们可以把内核态的线程依然叫做线程(thread),而用户态的线程我们可以称其为协程(co-routine)

看到这里,我们就很容易理解到,既然一个协程必须绑定一个线程,那么是不是意味着多个协程可以绑定同一个线程呢。答案是可以的。此时协程在用户态即可完成切换工作,不会陷入内核态切换,这种切换是非常快捷的。现在我们就得到了协程调度模型图

image-20210712094815441

Goroutine

Gotoutine 来自协程概念,它是用户态的线程,可以让一组可服用的函数运行在一组内核态的线程中。一个 Goroutine的初始空间大概只有4KB左右,并且在这4KB内存就足够一个函数完成运行,当然 Goroutine 的内存也是可以扩容的,最大可以扩容到1GB。

GM模型早在很久之前就被废弃了,没有撑到正式版本,接下来我们来了解一下GM模型是什么,

本文使用G来表示 Goroutine,使用M来表示线程

首先Go底层维护一个全局的协程队列,所有的G都会被放在这个全局队列中,同时多个M从全局队列中获取和放回G,也正因为如此,我们可以知道有多个线程访问同一个临界资源,这时候就需要对这个临界资源加锁,不然就会出现脏读等一系列问题。因此我们可以得出如下图

image-20210712140525051

该调度模型有几个缺点

  1. 多个M同时访问全局队列,无论是调度、放回、销毁一个 Goroutine 都需要获取锁,这样会形成激烈的锁竞争问题
  2. M移交G会照成延迟和额外的系统负载。比如当 G 中包含创建新协程的时候,M 创建了 G’,为了继续执行 G,需要把 G’交给 M’执行,也造成了很差的局部性,因为 G’和 G 是相关的,最好放在 M 上执行,而不是其他 M’
  3. 系统调用 (CPU 在 M 之间的切换) 导致频繁的线程阻塞和取消阻塞操作增加了系统开销

GMP模型

面对之前的GM调度模型的一些问题,Go设计了新的调度器。在新调度器中,除了M和G,我们新引入了P。P中包含了运行Goroutine 的资源,如果线程M想要执行G,就必须要先获取P,P中还包含了可运行的G队列。

在GMP模型中,G是协程实体,线程M是运行G的实体,而调度器P的功能是把可以运行的G分配到工作线程中。接下来我们来看下GMP模型的整体运行图

假装有图片

我们来一一介绍一下图中的内容

  1. G全局队列:存放着等待运行的所有G
  2. P的本地队列:同G全局队列,存放的是P的待运行队列,可以存放的最大容量为256个。在G需要新建协程G’时,会优先加入本地队列中,如果发现本地队列已满则会放回一半到全局队列中
  3. P:所有的P会在程序启动时创建,最多存在 GOAXPROCS
  4. M:线程想要运行任务就需要获取P,从P的本地列表中取出G,如果发现本地列表为空,则优先偷取其余P的本地列表的一半,如果无法偷取则从全局列表中获取G,M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去

PM创建问题

P何时创建: 在程序启动的时候创建

M何时创建:当没有足够的M来关联P的时候,比如当前所有的M都阻塞了

调度器的设计主要从 线程复用并行利用 两个方面来优化调度

  1. 线程复用:避免对M的频繁创建、销毁。
    1. work stealing 机制:当本线程无可以运行的G时,尝试从其余P对立中偷取G,而不是销毁线程
    2. hand off 机制:当线程M因为G进行系统调用阻塞时,线程会主动释放当前的P,把P交由其余M执行,如果没有多余的M的话会创建新线程M。
  2. 并行利用
    1. 多调度器:最多有 GOMAXPROCS 个P。
    2. 抢占:在Go中,一个Goroutine最多占用CPU10ms的时间,防止其余Goroutine被饿死,
    3. 全局G队列:在新的GMP模型中依然保留了全局队列,但是作用被大大削弱

这里介绍一下GMP的调度过程,主要是我们在运行go func(){}的是否发生了什么。

  1. go func(){}执行
  2. 尝试直接加入P的局部队列,如果发现局部队列已满则加入全局G队列
  3. P尝试从局部队列中获取一个G执行,如果发现局部队列为空,则尝试从其余P中偷取一部分G或者直接从全局G队列中获取一批G
  4. M执行P中的函数,如果运行结束就销毁G
  5. 如果在执行期间发生了系统调用阻塞,则尝试用休眠M队列中获取一个M,如果未能获取到则会新增一个M
  6. 新建的M会接管当前阻塞的P。
  7. 当系统调用阻塞结束之后,这个G会尝试获取一个空闲P执行,并放入到这个P的本地队列中。如果获取不到P,那么M就会变成休眠状态,加入到空闲线程队列中,G会被放入到全局队列中

调度器的生命周期

  1. 创建第一个线程M0
  2. 创建第一个Goroutine G0
  3. 关联M0和G0
  4. 调度初始化,包括创建P等
  5. 创建main()函数中的 Goroutine
  6. M是否可以通过P获取到一个G,如果不能获取的话则休眠当前M,等待唤醒到第六步
  7. M设置环境变量,包括栈、程序计数器等
  8. G退出,此时M重新跳到第7步

这里说一下特殊的M0和G0。

  1. M0:M0是启动程序后的编号为0的主线程,这个 M 对应的实例会在全局变量 runtime.m0 中,不需要在 heap 上分配,M0 负责执行初始化操作和启动第一个 G, 在之后 M0 就和其他的 M 一样了。
  2. G0:G0是每启动一个M都是第一个创建的 GroutineG0仅用于负责调度的G,它不指向任何可以执行的函数,每一个M都拥有自身的G0,在调度或者系统调用时会使用G0的栈空间。全局变量的G0是M0的G0

我们来跟踪一段代码

package main

import "fmt"

func main() {
    fmt.Println("Hello world")
}

接下来我们来针对上面的代码对调度器里面的结构做一个分析。

也会经历如上图所示的过程:

  1. runtime 创建最初的线程 m0 和 goroutine g0,并把 2 者关联。
  2. 调度器初始化:初始化 m0、栈、垃圾回收,以及创建和初始化由 GOMAXPROCS 个 P 构成的 P 列表。
  3. 示例代码中的 main 函数是 main.main,runtime 中也有 1 个 main 函数 ——runtime.main,代码经过编译后,runtime.main 会调用 main.main,程序启动时会为 runtime.main 创建 goroutine,称它为 main goroutine 吧,然后把 main goroutine 加入到 P 的本地队列。
  4. 启动 m0,m0 已经绑定了 P,会从 P 的本地队列获取 G,获取到 main goroutine。
  5. G 拥有栈,M 根据 G 中的栈信息和调度信息设置运行环境
  6. G 退出,再次回到 M 获取可运行的 G,这样重复下去,直到 main.main 退出,runtime.main 执行 Defer 和 Panic 处理,或调用 runtime.exit 退出程序。

Go 调度器很轻量也很简单,足以撑起 goroutine 的调度工作,并且让 Go 具有了原生(强大)并发的能力。Go 调度本质是把大量的 goroutine 分配到少量线程上去执行,并利用多核并行,实现更强大的并发。

本文大量参考了Aceld的文章并进行个人总结,下面是作者的原文信息,写的非常精彩。


原文作者:Aceld
转自链接:https://learnku.com/articles/41728
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请保留以上作者信息和原文链接。

2021-07-12 16:31

没想到上周五开篇的现在才写完,懒死我自己。。。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK