一、GMP 调度模型概述
- 背景介绍
- 在 Go 语言中,为了高效地利用多核处理器并管理大量的并发任务,引入了 GMP 调度模型。它是 Go 语言运行时(runtime)系统的核心部分,用于管理和调度 Go 协程(goroutine)。
- 与传统的操作系统线程调度相比,Go 的 GMP 调度模型能够在少量操作系统线程(OS Thread)的基础上,高效地调度大量的轻量级协程,从而实现高性能的并发处理。
- 基本概念
- G(Goroutine):Go 语言中的协程,是一种轻量级的用户态线程。它由 Go 运行时进行调度,相比操作系统线程,其创建和销毁的成本非常低。例如,在一个网络服务器应用中,可以轻松创建数千个协程来处理并发的客户端连接,而不会像创建数千个操作系统线程那样带来巨大的资源开销。
- M(Machine):代表操作系统线程。Go 运行时会将协程调度到操作系统线程上执行。M 与操作系统的线程一一对应,它是真正执行计算的实体。
- P(Processor):可以看作是逻辑处理器。它是连接 G 和 M 的桥梁,每个 P 都有一个本地队列,用于存放等待执行的 G。P 的数量通常与系统的 CPU 核心数相关,通过这种方式可以充分利用多核处理器的性能。
二、GMP 调度模型的工作流程
- G 的创建与初始化
- 当使用
go
关键字创建一个协程时,Go 运行时会分配一个 G 结构体来表示这个协程。这个 G 结构体包含了协程的栈空间、程序计数器、状态等信息。 - 例如,
go func() { println("Hello, world") }()
这个语句会创建一个新的协程,该协程会在合适的时候执行println("Hello, world")
这个函数。
- G 进入 P 的本地队列
- 新创建的 G 会被放入一个 P 的本地队列中。如果本地队列已满,它可能会被放入全局队列或者其他的本地队列中。
- 每个 P 会从自己的本地队列中取出 G 来执行。这样可以减少锁的竞争,提高调度效率,因为每个 P 在大部分时间里都可以独立地从自己的本地队列中获取 G 进行处理。
- M 与 P 的关联和 G 的执行
- 当一个 M 空闲时,它会尝试获取一个 P。如果获取到 P,M 会从 P 的本地队列中取出一个 G 来执行。
- 在执行 G 的过程中,如果 G 执行了阻塞操作(如系统调用、通道操作等),M 可能会将当前的 G 暂停,并尝试从 P 的本地队列中获取下一个 G 来执行。如果 P 的本地队列中没有 G,M 可能会从全局队列或者其他 P 的本地队列中获取 G。
- 调度循环
- M 会不断地从 P 的本地队列或者其他地方获取 G 并执行,这个过程形成一个调度循环。当 G 执行完成后,M 会将 G 标记为已完成,并可能会从本地队列或者其他地方获取下一个 G 继续执行。
三、GMP 调度模型的源码解析
- 关键数据结构
runtime.h
中的定义:
- 在 Go 的源码中,
runtime.h
文件包含了很多关键的数据结构定义。例如,struct G
结构体定义了协程的各种属性,包括栈指针、状态、调度相关的信息等。 struct M
结构体定义了与操作系统线程对应的信息,如线程 ID、当前正在执行的 G 等。struct P
结构体定义了处理器相关的信息,如本地队列的指针、状态等。
- 调度函数
schedule()
函数:
- 这是 Go 运行时调度的核心函数之一。它主要负责选择下一个要执行的 G。它会首先检查当前 P 的本地队列,如果本地队列中有 G,就从中取出一个 G 进行执行。
- 如果本地队列中没有 G,它会尝试从全局队列中获取 G。在获取 G 的过程中,会涉及到一些锁的操作,以确保全局队列的并发安全。
findrunnable()
函数:
- 这个函数用于寻找一个可运行的 G。它会综合考虑本地队列、全局队列以及其他可能的来源(如从其他 P 的本地队列中 “窃取” G)来找到一个可以执行的 G。它的实现细节涉及到复杂的队列操作和调度策略。
- 协程的创建和初始化源码
- 在
runtime/proc.go
文件中,newproc()
函数负责创建新的协程。它会分配一个新的G
结构体,初始化其栈空间和相关的参数,然后将其放入合适的队列(通常是 P 的本地队列)中等待执行。 - 例如,
newproc()
函数中的部分代码用于设置新协程的栈空间大小和栈顶指针等,确保协程在执行时有足够的空间来存储局部变量和函数调用信息。
- 与操作系统线程的交互
- Go 语言的运行时需要与操作系统线程进行交互。在
runtime/os_linux.go
(以 Linux 系统为例)等文件中,有代码用于创建和管理操作系统线程。 - 当一个 M 需要创建一个操作系统线程时,会调用相关的系统调用(如
clone()
函数)来创建一个新的线程。同时,在这个线程的启动函数中,会调用 Go 运行时的调度函数,使得这个线程能够参与到协程的调度过程中。
通过深入研究 Go 语言的 GMP 调度模型和相关源码,可以更好地理解 Go 语言高效并发处理的机制,并且在开发高性能的并发程序时能够更加得心应手。