GMP模型 Reference
- GMP 并发调度器深度解析之手撸一个高性能 goroutine pool - Strike Freedom
- [Golang三关-典藏版] Golang 调度器 GMP 原理与调度全分析 | Go 技术论坛
- Golang深入理解GPM模型\_哔哩哔哩\_bilibili
- Golang 深入理解GMP\_郭传志的技术博客\_51CTO博客
- GMP模型 — 深入Go语言之旅
- 深入理解 Go | 调度:GMP 模型(第三部分) | ∞
基本概念
Go的GMP是指它的调度器(scheduler)使用的一种并发模型,也称为M:N调度器。在这种模型中,M个goroutine(Go协程)被映射到N个操作系统线程(OS threads)上,其中M和N都是可配置的。
GMP的工作原理如下:
- G:代表Goroutine,是Go语言中轻量级的执行单元;
- M:代表Machine,是Go语言中的执行线程,负责执行Goroutine;
- P:代表Processor,是Go语言中的处理器,负责调度Goroutine。
在GMP模型中,所有的Goroutine都被放在一个全局的队列中。当一个M空闲时,它会从队列中取出一个Goroutine并执行它。如果一个Goroutine需要进行阻塞操作(如等待I/O完成),它会将自己从M上解绑,并将M还给调度器。此时,调度器会找到另一个空闲的M,并将其绑定到这个Goroutine上。当阻塞操作完成后,这个Goroutine会重新被放回队列中等待执行。
每个M都有一个本地队列,用于存放它当前正在执行的Goroutine。当一个M执行完当前的Goroutine后,它会从本地队列中取出下一个Goroutine执行。如果本地队列为空,它会从全局队列中获取Goroutine并执行。如果全局队列也为空,它会进入休眠状态等待新的Goroutine加入全局队列。
P是调度器用来管理M和Goroutine之间关系的数据结构。P的数量可以动态调整,调度器会根据当前系统负载情况动态增加或减少P的数量。每个P都有一个本地队列和一个全局队列。当一个Goroutine需要执行时,调度器会将其放入某个P的本地队列中。如果本地队列已满,它会将Goroutine放入全局队列中等待其他P来处理。
总的来说,GMP模型通过将大量的Goroutine映射到少量的OS线程上,实现了高效的并发处理。同时,调度器可以动态调整M和P的数量,适应不同的系统负载情况,从而提高了系统的性能和可伸缩性。
此外,GMP模型还具有以下优点:
- 轻量级:Goroutine是非常轻量级的执行单元,可以快速地创建和销毁,不会占用太多的系统资源。
- 高效:GMP模型中的调度器采用了一些高效的算法,如自适应调度、工作窃取等,可以尽可能地利用系统资源,提高系统的吞吐量和响应速度。
- 安全:GMP模型中的调度器可以保证多个Goroutine之间的安全性,避免了一些常见的并发问题,如死锁、竞态条件等。
当然,GMP模型也存在一些缺点。例如,由于每个M都需要维护自己的本地队列,这可能会导致一些负载不均衡的情况。此外,由于M和P的数量是可配置的,如果配置不当可能会导致性能下降。因此,在使用GMP模型时需要根据具体情况进行调整和优化。
调度器整体梳理:
- 将协程放入每个处理器的本地队列中,满了放了全局队列。
- 然后处理器会和空闲的内核线程做绑定,运行处理器中的协程。
- 如果某个线程阻塞了,处理器会和当前线程解除绑定,去和其他空闲的线程绑定。如果没有线程了,处理器可以创建线程。
调度器的设计策略
复用线程:避免频繁的创建、销毁线程,而是对线程的复用。
- work stealing 机制:当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。
- hand off 机制:当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。
- 利用并行:GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。GOMAXPROCS 也限制了并发的程度,比如 GOMAXPROCS = 核数/2,则最多利用了一半的 CPU 核进行并行。
- 抢占:在 coroutine 中要等待一个协程主动让出 CPU 才执行下一个协程,在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死,这就是 goroutine 不同于 coroutine 的一个地方。
- 全局 G 队列:在新的调度器中依然有全局 G 队列,但功能已经被弱化了,当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G。
M运行G的时候:
- 正常结束
- 阻塞:M与P接触绑定,P与其他空闲M解绑。
- 执行时间到:重新放入队列。
go func调度流程
M0
M0 是启动程序后的编号为 0 的主线程,这个 M 对应的实例会在全局变量 runtime.m0 中,不需要在 heap 上分配,M0 负责执行初始化操作和启动第一个 G, 在之后 M0 就和其他的 M 一样了。
G0
G0 是每次启动一个 M 都会第一个创建的 goroutine,G0 仅用于负责调度的 G,G0 不指向任何可执行的函数,每个 M 都会有一个自己的 G0。在调度或系统调用时会使用 G0 的栈空间,全局变量的 G0 是 M0 的 G0。
代码分析
package main
import "fmt"
func main() {
fmt.Println("Hello world")
}
M0是一般是和main goroutine绑定,因为它第一个获取到P
- runtime 创建线程 m0 和 goroutine g0,并关联。
- 调度器初始化:初始化 m0、栈、垃圾回收。创建和初始化调度器 P。
- runtime.main 调用 main.main,为 runtime.main 创建 goroutine并加到 P 的本地队列。
- 启动 m0,m0 已经绑定了 P,会从 P 的本地队列获取 G,获取到 main goroutine。
- G 拥有栈,M 根据 G 中的栈信息和调度信息设置运行环境。
- M 运行 G。
- G 退出,再次回到 M 获取可运行的 G,这样重复下去,直到 main.main 退出,runtime.main 执行 Defer 和 Panic 处理,或调用 runtime.exit 退出程序。
GMP可视化
package main
import (
"os"
"fmt"
"runtime/trace"
)
func main() {
//创建trace文件
f, err := os.Create("trace.out")
if err != nil {
panic(err)
}
defer f.Close()
//启动trace goroutine
err = trace.Start(f)
if err != nil {
panic(err)
}
defer trace.Stop()
//main
fmt.Println("Hello World")
}
分析:
go run trace.go
go tool trace trace.out
main函数为trace.Start.func1函数创建了一个协程G6,放入P1中,然后P1创建M2来运行。