Go 语言入门与进阶: MPG 模型概述

2,983 阅读4分钟

这是我参与更文挑战的第 26 天,活动详情查看: 更文挑战

前文回顾

如果你还没有 Go 语言基础,建议阅读我的 从零学 Go

本系列文章,我将会进一步加深对 Go 语言的讲解,更一步介绍 Go 中的包管理、反射和并发等高级特性。

前面一篇文章主要介绍了 Go 语言常见的线程模型。本文将会介绍 Go 中的 MPG 线程模型,该线程模型对两级线程模型进行一定程度的改进。

MPG 模型概述

Go 中的 MPG 线程模型对两级线程模型进行一定程度的改进,使它能够更加灵活地进行线程之间的调度。它由三个主要模块构成,如图所示:

MPG线程模型.png

  • machine,一个 machine 对应一个内核线程,相当于内核线程在 Go 进程中的映射
  • processor,一个 prcessor 表示执行 Go 代码片段的所必需的上下文环境,可以理解为用户代码逻辑的处理器
  • goroutine,是对 Go 中代码片段的封装,其实是一种轻量级的用户线程。

为了减轻描述工作,下面的介绍中我们会用 M、P、G 分别指代 machine、processor 和 goroutine。从图中可以看出:

每一个 M 都会以一个内核线程绑定,M 和 P 之间也是一对一的关系,而 P 和 G 的关系则是一对多。在运行过程中,M 和 内核线程之间对应关系的不会变化,在 M 的生命周期内,它只会与一个内核线程绑定,而 M 和 P 以及 P 和 G 之间的关系都是动态可变的。

在实际的运行过程中,M 和 P 的组合才能够为 G 提供有效的运行环境,而多个可执行 G 将会顺序排成一个队列挂在某个 P 上面,等待调度和执行,如下图所示:

MPG.png

上图中,M 和 P 共同构成了一个基本的运行环境,此时 G0 中的代码片段处于正在运行的状态,而右边的 G 队列处于待执行状态。

M 的创建一般是因为没有足够的 M 来和 P 组合以为 G 提供运行环境,在很多时候 M 的数量可能会比 P 要多。在单个 Go 进程中,P 的最大数量决定了程序的并发规模,且 P 的最大数量是由程序决定的。可以通过修改环境变量 GOMAXPROCS 和 调用函数 runtime#GOMAXPROCS 来设定 P 的最大值。

M 和 P 会适时的组合和断开,保证 P 中的待执行 G 队列能够得到及时运行。比如说上图中的 G0 此时因为网络 I/O 而阻塞了 M,那么 P 就会携带剩余的 G 投入到其他 M 的怀抱中。这个新的 M1 可能是新创建的,也可能是从调度器空闲 M 列表中获取的,取决于此时的调度器空闲 M 列表中是否存在 M,从而避免 M 的过多创建,如图所示:

M和G组合与断开.png

当 M 对应的内核线程被唤醒时,M 将会尝试为 G0 捕获一个 P 上下文,可能是从调度器的空闲 P 列表中获取,如果获取不成功,M 会被 G0 放入到调度器的可执行 G 队列中,等待其他 P 的查找。为了保证 G 的均衡执行,非空闲的 P 会运行完自身的可执行 G 队列中,会周期性从调度器的可执行 G 队列中获取代执行的 G,甚至从其他的 P 的可执行 G 队列中掠夺 G!

小结

本文主要介绍了 Go 语言中的 MPG 线程模型。Go 语言并发的真理是:不要以共享内存的方式来通信,相反,要通过通信来共享内存。当一个 P 关联多个 G 时,就会处理 G 的执行顺序,就是并发,当一个 P 在执行一个协程工作时,其他的会在等待,当正在执行的协程遇到阻塞情况,例如 IO 操作等,Go 的处理器就会去执行其他的协程,因为对于类似 IO 的操作,处理器不知道你需要多久才能执行结束,所以他不回去等你执行完。

Go 的并发好像是抢占式的,事实上协程是非抢占式的,由协程主动交出控制权,也就是说,上面在发生 IO 操作时,并不是调度器强制切换执行其他的协程,而是当前协程交出了控制权,调度器才去执行其他协程。

阅读最新文章,关注公众号:aoho求索