Golang-GMP模型
创始人
2024-06-01 07:45:11
0

写在前面

Go 为了自身 goroutine 执行和调度的效率,自身在 runtime 中实现了一套 goroutine 的调度器,下面通过一段简单的代码展示一下 Go 应用程序在运行时的 goroutine,方便大家更好的理解。

The Go scheduler is part of the Go runtime, and the Go runtime is built into your application

for i := 0; i < 4; i++ {go func() {time.Sleep(time.Second)}()
}
fmt.Println(runtime.NumGoroutine())

上面这段代码的输出为:5 说明当前这个应用程序中存在 goroutine 的数量是 5,事实上也符合我们的预期。那么问题来了,这 5 个 goroutine 作为操作系统用户态的基本调度单元是无法直接占用操作系统的资源来执行的,必须经过内核级线程的分发,这是操作系统内部线程调度的基本模型,根据用户级线程和内核级线程的对应关系可以分为 1 对 1,N 对 1 以及 M 对 N 这三种模型,那么上述的 5 个 goroutine 在内核级线程上是怎么被分发的,这就是 Go语言的 goroutine 调度器决定的。

GMP 模型

整个 goroutine 调度器的实现基于 GMP 的三级模型来实现。

  • G:goroutine (go 代码)
  • M:内核级线程,运行在操作系统的核心态。(在 Go 中支持最大的 M 的数量是 10000,但是操作系统中通常情况是不可以创建这么多的线程。)
  • P:processor,可以理解成一个等待分发给 M 调度执行的 goroutine 队列。(P的个数是由 runtime 的 GOMAXPROCS 来决定的。)

M 和 P 存在一一对应的绑定关系。大致的结构图如下所示:

GMP 模型图如下:

  1. 全局队列(Global Queue):存放等待运行的 G。
  2. P 的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量有限,不超过 256 个。新建 G’时,G’优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。
  3. P 列表:所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。
  4. M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。

Goroutine 调度器和 OS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行。

goroutine 之旅

通常情况下,我们在代码中执行 go func(){}后,GMP 模型是如何工作的?通过一个详细的图来展示一下。

  1. 首先创建一个新的 goroutine
  2. 如果本地的局部队列中有足够的空间可以存放,则放入局部队列中;如果局部队列满,则放入一个全局队列(所有的 M 都可以从全局队列中拉取 G 来执行)
  3. 所有的 G 都必须在 M 上才可以被执行,MP 存在一一绑定的关系,如果 M 绑定的 P 中存在可以被执行的 G,则从 P 中拉取 G 来执行;如果 P 中为空,没有可执行的 G,则 M 从全局队列中拉取;如果全局队列也为空,则从其他的 P 中拉取 G
  4. G 的运行分配必要的资源,等待 CPU 的调度
  5. 分配到 CPU,执行 func(){}

调度策略

整个 goroutine 调度器最重要的调度策略是:复用,避免频繁的资源创建和销毁,最大限度的提升系统的吞吐量和并发程度。这也是操作系统进行线程调度的终极目标。复用(reuse)也是很多「池化技术」的基础。

围绕着这一原则,goroutine 调度器在以下几个方面进行调度策略的优化。

  1. 工作队列的窃取机制:这个跟 Java 中的 ForkJoin Pool 的窃取机制同一原理,都是当线程 M 空闲时,从其他繁忙的队列 P 中"窃取"任务 G 过来执行,而不是销毁空闲的 M。因为线程的创建和销毁是需要消耗系统资源的,避免线程的频繁创建和销毁可以极大的提升系统的并发程度。
  2. 交接机制:当线程M被阻塞的时候,M 会主动将 P 交接给其他空闲的 M

另外,在 go 的 1.14 版本中,go 语言的技术团队尝试在调度器中添加了可抢占的技术. (https://github.com/golang/go/issues/24543)[https://github.com/golang/go/issues/24543]

抢占技术的出现一方面解决了线程 M 在执行计算密集型任务时长时间占用 CPU,导致与之绑定的 P 上的其他 G 得不到执行而造成的"饥饿现象";
另一方面,抢占技术的出现对 GC 来讲解决 GC 时可能出现的 deadLock,相关的 issue 见:关于 GC 时 tight loops 应该可以被抢占的讨论(https://github.com/golang/go/issues/10958)[https://github.com/golang/go/issues/10958]

调度器的生命周期

特殊的 M0 和 G0

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

最开始的 MG 模型

在 go 语言的早期,goroutine 调度器的模型并不是 GMP,而是 GM。整个调度器维护一个全局的 G 的等待队列,所有的 M 从这个全局的队列中拉取 G 来执行,在 go1.1 中将这种模型直接干掉,取而代之的是现在的 GMP 模型,在 GM 模型的基础上增加 P 局部队列。官方之所有这么这么做,原因有三:

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

小结

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

参考

  • [Golang三关-典藏版] Golang 调度器 GMP 原理与调度全分析
  • Go Scheduler 的 GMP 模型

相关内容

热门资讯

韩国特检组向法院申请拘留前总统... 转自:中安在线总台记者获悉,当地时间6日下午,韩国内乱特检组以妨碍公务、伪造公文等嫌疑要求法院拘留前...
杜锋:在全运舞台续写广东篮球辉... 编者按:第十五届全国运动会将于2025年11月在粤港澳三地举行。这是一场体育盛会,更是推动全民健身、...
​任刚到莱西市调研时强调:坚持... 青岛日报社/观海新闻7月6日讯  5日,市委副书记、市长任刚率市直有关部门负责同志到莱西市调研。他强...
景县总工会创新就业服务模式,让... 转自:河工新闻网  河工新闻网讯(记者李昊男 通讯员葛志强)“能带我们看看车间里面是什么样吗?”“大...
韩媒:韩国内乱独检组提请法院对... 中新网7月6日电 据韩联社报道,当地时间6日,韩国内乱独检组提请法院对前总统尹锡悦签发逮捕令。资料图...
纳尔股份92万股限制性股票7月... 7月6日,纳尔股份(002825)发布公告,近日完成了2025年限制性股票激励计划的首次授予登记工作...
重获“心”生的青海小球迷如愿来... 转自:交汇点新闻 交汇点讯 “我爱江苏,南哥加油!”7月...
月入2万的10个小生意(县城创... 当今社会,各行业竞争激烈。没钱赚很多钱不容易。有很多在高层工作的白领,看起来光彩照人,但实际上只是工...
南京一小区业委会花近千万维修金... 据荔枝新闻7月6日报道,此前,南京的某小区因年久失修,高层消防系统无法正常使用,业委会动用维修基金5...
年报难产,停牌两个月,A股百亿... 每经编辑|黄胜     7月6日晚间,天茂集团(000627.SZ,股价2.74元...