Golang协程调度GMP模型 | 豆包MarsCode AI 刷题

104 阅读4分钟

GMP简介

在 Go 中,当你启动多个 goroutine(例如 10 个),这些 goroutine会被 Go 语言的 调度器(scheduler)管理和调度。Go的调度器运行在 用户态,它不依赖操作系统的线程调度来管理 goroutine,而是自行管理和调度。

Go 的调度器采用的是一种称为 GMP 模型 的设计,其中包括以下几个概念:

  1. G(Goroutine) :表示一个具体的 goroutine,包含该 goroutine 的堆栈、任务代码和状态信息。
  2. M(Machine) :表示一个操作系统线程,负责执行具体的 goroutine。在 Go 中,M 是真正的系统线程,由操作系统内核态管理。
  3. P(Processor) :表示一个逻辑处理器,是一个调度上下文。P负责调度并管理 goroutine。只有当 M 绑定了 P后,才能执行 goroutine。

调度过程基于 GMP 模型进行,其中:

  • 每个 P 持有一个本地运行队列,用于存放待执行的 goroutine
  • M(系统线程)只有在绑定 P 时才会执行 Ggoroutine)。
  • PM 的数量可以根据 GOMAXPROCS 设置,限制 Go运行时能并行执行的 goroutine数量。

调度机制

当我们启动 10 个 goroutine时,调度过程如下:

  1. 初始化 P 的数量

    • GOMAXPROCS 设置了并行执行的 P 数量,默认是 CPU 核心数。
    • 假设 GOMAXPROCS=4,则会有 4 个 P,即 4 个逻辑处理器可以并行调度 goroutine
  2. 创建 goroutine(G)并放入 P 的本地队列

    • go 关键字启动 goroutine时,Go调度器会为每个 goroutine创建一个 G 对象并放入某个 P 的本地队列。
    • 例如,假设我们创建了 10 个 goroutine,它们会分配到 4 个 P 的本地队列中,等待执行。
  3. 调度和执行

    • 每个 M 线程(系统线程)绑定一个 P 后会从 P 的本地队列中获取 G 并开始执行。
    • M 会在用户态下执行 Ggoroutine),当发生阻塞操作(如 I/O 或系统调用)时,M 会进入内核态,由操作系统调度。
    • 如果某个 G 阻塞在系统调用上,Go运行时会解绑定该 MP,并创建一个新的 M(新线程)绑定到该 P 上,确保其他 G 可以继续执行。
  4. 抢占式调度

    • Go 1.14 引入了抢占式调度。运行时会定期检查长时间运行的 goroutine,并尝试将其挂起,重新分配给其他 goroutine,从而防止某个 goroutine长时间占用 P
    • 这种调度在用户态完成,不涉及内核态操作。
  5. 工作窃取(Work Stealing)

    • 当某个 P 的本地队列为空时,它会尝试从其他 P 的本地队列中窃取 G。这种工作窃取机制提高了任务分配的均衡性。
    • 如果本地队列和其他 P 的队列都没有待执行的 G,该 M 会进入休眠状态,等待新的任务或其他 P 的任务窃取。

用户态与内核态的关系

  • 用户态执行Go调度器运行在用户态,负责管理 goroutine的创建、分配和调度,不依赖操作系统的线程调度。Goroutine切换是在用户态完成的,开销较小,因为不需要操作系统的内核态参与。
  • 进入内核态的情况:当 goroutine执行阻塞操作(如文件 I/O、网络 I/O 或系统调用)时,当前 M 会进入内核态,等待 I/O 完成。在这种情况下,Go调度器会解绑定该 MP,并将 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数。