Skip to content

Latest commit

 

History

History
144 lines (110 loc) · 10.1 KB

go-gmp.md

File metadata and controls

144 lines (110 loc) · 10.1 KB

GO 系列 - GMP 调度器

Scheduler

GO 程序和操作系统之间的关系

2024-08-04T230700

通过上图我们可以看到,操作系统内核和我们写的逻辑代码之间通过 runtime 进行交互,runtime 会调用操作系统的系统调用,操作系统会调用硬件资源,这样我们的程序才能运行。

所谓的 Scheduler 就是负责调度 goroutine 的模块,它会根据一定的策略来调度 goroutine 的执行,这样才能保证 goroutine 的执行顺序和并发度。其使用的模型是 GMP 模型。

GMP 模型

GMP 模型是 Go 语言调度器的核心模型,它是 Go 语言调度器的基础。GMP 模型是指:

组件 描述
G (Goroutine) 表示一个 Goroutine,包含栈和相关的上下文信息
M (Machine) 表示一个执行线程,负责将 Goroutine 映射到操作系统的线程上。每个 M 都有自己的调用栈和寄存器状态
P (Processor) 表示一个逻辑处理器,维护一个处于可运行状态的 Goroutine 队列,每个 M 都和一个 P 相关联

2024-08-04T230800

提示:

  • 全局队列:存放所有正在等待运行的 G
  • 本地队列:存放当前 PG 每个 P 都有一个本地队列, 用于存放当前 P 等待和正在运行的 G,每个 P 的本地队列中最多存放 256G 。创建 G 时,会优先放入本地队列,如果本地队列满了, 则会将队列中一半的 G 移动到全局队列中。

注意

  • P 的数量P 的数量是固定的,由 GOMAXPROCS 决定,即最大并发数, 默认为 CPU 核数。
  • M 的数量M 的数量是动态的,由调度器决定,根据当前的负载情况动态调整, GO默认设置为 10000,实际上内核很难达到该限制,可以认为是没有限制。M 想要运行任务就需要获取 P,如果没有 PM 就会阻塞。如果 P 的本地队列为空,M 会从全局队列中获取 G,放入本地队列。 如果全局队列也为空,M 会从其他随机一个 P 的本地队列中获取一半的 G 放到本地队列中。

名词缩写

  • LRQ (local runnable queue): 本地队列
  • GRQ (global runnable queue): 全局队列

GMP 数据结构

重要字段

字段 类型 描述
g0 *g 每个 M 都会有一个 g0
curg *g 正在运行的 G
p puintptr 绑定的 P
nextp puintptr 当 M 被唤醒时,优先绑定的 P
id int64 M 的 ID
spinning bool 是否处于自旋状态
park note 用于 M 的休眠和唤醒
alllink *m 在 allm 链表上的 M
schedlink muintptr 下一个 M,构成 M 链表
mcache *mcache 内存分配的 M 缓存大小
lockedg guintptr 是否被锁,锁的指针
freelink *m 在 sched.freem 上的 M

重要字段

字段 类型 描述
id int32 P 的唯一标识符
status uint32 P 的状态
link puintptr 下一个 P,在 P 链表中
m muintptr 拥有这个 PM
mcache *mcache 用于内存分配的 P 本地缓存
runqhead uint32 P 本地 runnable 状态的 G 队列头部,无锁访问
runqtail uint32 P 本地 runnable 状态的 G 队列尾部,无锁访问
runq [256]guintptr P 本地 runnable 状态的 G 队列,最多 256 个元素
runnext guintptr 一个比 runq 优先级更高的 runnable G
gFree struct 状态为 dead 的 G 链表,在获取 G 时会从这里面获取。
gcBgMarkWorker guintptr (原子操作) 用于 GC 后台标记的 worker
gcw gcWork 用于 GC 的工作结构体。

P 的5个状态

状态字段 编号 描述
_Pidle 0 表示 P 当前未被用于运行用户代码或调度程序。通常,它在空闲 P 列表上,并可供调度程序使用,但它可能正处于其他状态之间的过渡中。P 属于空闲列表或正在过渡其状态的任何其他实体。其运行队列为空。
_Prunning 1 表示 P 属于某个 M 并用于运行用户代码或调度程序。只有拥有此 P 的 M 允许从 _Prunning 更改 P 的状态。M 可以将 P 过渡到 _Pidle(如果没有更多工作要做),_Psyscall(进入系统调用时)或 _Pgcstop(用于 GC 停顿)。M 还可以直接将 P 的所有权移交给另一个 M(例如,以调度锁定的 G)。
_Psyscall 2 表示 P 当前未运行用户代码。它与某个 M 在系统调用中具有亲和性,但不由该 M 拥有,并且可能被其他 M 抢占。这类似于 _Pidle,但使用轻量级过渡并保持 M 亲和性。离开 _Psyscall 必须使用 CAS 完成,以便抢占或重新获取 P。请注意,存在 ABA 风险:即使 M 在系统调用后成功 CAS 回其原始 P 为 _Prunning,它也必须理解 P 可能在此期间被其他 M 使用过。
_Pgcstop 3 表示 P 已停止用于 STW 并由停止世界的 M 拥有。停止世界的 M 继续使用其 P,即使在 _Pgcstop 中也是如此。从 _Prunning 过渡到 _Pgcstop 会导致 M 释放其 P 并进行休眠。P 保留其运行队列,startTheWorld 将在具有非空运行队列的 P 上重新启动调度程序。
_Pdead 4 表示 P 不再使用(GOMAXPROCS 减少)。如果 GOMAXPROCS 增加,我们会重用 P。已死亡的 P 大部分资源已被剥离,尽管仍有一些资源保留(例如,跟踪缓冲区)。

调度器策略

策略 描述
抢占式调度 在协程中需要一个协程主动让出 CPU 下一个协程才能使用 CPU, 而 Goroutine 规定一个 Goroutine 每次最多只能占用 10ms 的 CPU,然后就要切换到下一个, 防止其他协程长时间不被执行
复用线程 Go 语言的调度器会复用线程,而不是每次都创建新的线程,这样可以减少线程创建和销毁的开销,提高性能。 - 工作偷取(Work stealing):当 M 没有可运行的 G 时,会尝试从其他线程绑定的 P 的本地队列中偷取一半的 G来运行,而不是销毁 M - 挂起机制(Hand off): 当 G 由于系统调用而阻塞时, M 会释放绑定的 P 供其他 M 使用
并行 通过 GOMAXPROCS 配置 P 的数量,从而实现并行执行,P 的数量决定了并行度,P 的数量等于 CPU 核数时,可以实现最大并行度。
全局队列 当本地队列中没有可运行的 GM 会先去全局队列尝试获取 G, 若全局队列中没有待运行的 G 则会尝试去其他 P 的本地队列中偷取 G

GMP 调度流程

2024-08-04T231100

Goroutine 调度流程

  • 创建一个 G

如果本地队列没满,则随机放入一个未满的本地队列,否则放入全局队列。

  • 执行 G

P 会获取一个 GM 中执行,若 G 产生 systemCall 阻塞,则会将 M 放入休眠队列,并从休眠队列中取出一个 M 接管 P 执行,若休眠队列为空则创建一个新的 M 来接管 P

  • 获取 G

若本地队列中 G 已经执行完,则尝试从从全局队列中获取 G,若全局队列中没有可运行的 G, 则从其他 P 的本地队列中偷取 G

常见的阻塞

  • I/O Select
  • Block on syscall
  • Channel
  • Mutex
  • Sleep
  • runtime.Gosched()

M 的生命周期

2024-08-04T231300

M0 和 G0

对象 描述
M0 M0 是主线程创建的第一个线程,负责创建和运行第一个 G, 存储在 runtime.mO 中,不需要在 Heap 上分配
G0 M0 创建之后会立即创建一个 G0 , 这个 G0 只用于调度 G, 不执行逻辑代码, G0M0 对应

Read More:

GMP调度器