GMP简介
在 Go 中,当你启动多个 goroutine(例如 10 个),这些 goroutine会被 Go 语言的 调度器(scheduler)管理和调度。Go的调度器运行在 用户态,它不依赖操作系统的线程调度来管理 goroutine,而是自行管理和调度。
Go 的调度器采用的是一种称为 GMP 模型 的设计,其中包括以下几个概念:
- G(Goroutine) :表示一个具体的
goroutine,包含该goroutine的堆栈、任务代码和状态信息。 - M(Machine) :表示一个操作系统线程,负责执行具体的
goroutine。在 Go 中,M 是真正的系统线程,由操作系统内核态管理。 - P(Processor) :表示一个逻辑处理器,是一个调度上下文。
P负责调度并管理goroutine。只有当 M 绑定了P后,才能执行 goroutine。
调度过程基于 GMP 模型进行,其中:
- 每个
P持有一个本地运行队列,用于存放待执行的goroutine。 M(系统线程)只有在绑定P时才会执行G(goroutine)。P和M的数量可以根据GOMAXPROCS设置,限制Go运行时能并行执行的goroutine数量。
调度机制
当我们启动 10 个 goroutine时,调度过程如下:
-
初始化
P的数量:GOMAXPROCS设置了并行执行的P数量,默认是 CPU 核心数。- 假设
GOMAXPROCS=4,则会有 4 个P,即 4 个逻辑处理器可以并行调度goroutine。
-
创建 goroutine(G)并放入 P 的本地队列:
- 当
go关键字启动goroutine时,Go调度器会为每个goroutine创建一个G对象并放入某个P的本地队列。 - 例如,假设我们创建了 10 个
goroutine,它们会分配到 4 个P的本地队列中,等待执行。
- 当
-
调度和执行:
- 每个
M线程(系统线程)绑定一个P后会从P的本地队列中获取G并开始执行。 M会在用户态下执行G(goroutine),当发生阻塞操作(如I/O或系统调用)时,M会进入内核态,由操作系统调度。- 如果某个
G阻塞在系统调用上,Go运行时会解绑定该M和P,并创建一个新的M(新线程)绑定到该P上,确保其他G可以继续执行。
- 每个
-
抢占式调度:
Go 1.14引入了抢占式调度。运行时会定期检查长时间运行的goroutine,并尝试将其挂起,重新分配给其他goroutine,从而防止某个goroutine长时间占用P。- 这种调度在用户态完成,不涉及内核态操作。
-
工作窃取(Work Stealing) :
- 当某个
P的本地队列为空时,它会尝试从其他P的本地队列中窃取G。这种工作窃取机制提高了任务分配的均衡性。 - 如果本地队列和其他
P的队列都没有待执行的G,该M会进入休眠状态,等待新的任务或其他P的任务窃取。
- 当某个
用户态与内核态的关系
- 用户态执行:
Go调度器运行在用户态,负责管理goroutine的创建、分配和调度,不依赖操作系统的线程调度。Goroutine切换是在用户态完成的,开销较小,因为不需要操作系统的内核态参与。 - 进入内核态的情况:当
goroutine执行阻塞操作(如文件 I/O、网络 I/O 或系统调用)时,当前M会进入内核态,等待I/O完成。在这种情况下,Go调度器会解绑定该M和P,并将P分配给另一个空闲的M。当阻塞操作完成后,M返回用户态并尝试重新绑定P,以继续执行任务。 - 多线程并行:
M实际上是操作系统的线程,因此它们在多个CPU核心上并行运行时会利用内核态的调度器。Go运行时仅控制goroutine的并发和调度,而具体的线程分配到哪个CPU核心上是由操作系统的内核调度器决定的。
小结
Go调度器在用户态管理goroutine,通过P控制并发数量,通过M执行G。Goroutine的调度、抢占和工作窃取都是用户态的操作。- 只有当发生阻塞操作(如
I/O)时,M会进入内核态等待,而P可以被调度到其他空闲的M继续执行任务。 - 通过
GOMAXPROCS控制并发执行的goroutine数量,并发goroutine的数量会根据逻辑处理器P数量调整,但不限制创建的总goroutine数。