你以前可能听说过 Goroutine 调度器,但你对它的工作原理了解多少?它如何将 goroutine 与线程配对?
不用着急理解上面的图像,因为我们要从最基本的开始。
goroutine 被分配到线程中运行,这由 goroutine 调度器在后台处理。根据我们之前的讨论,我们了解到以下关于 goroutine 的几点:
-
就原始执行速度而言,goroutine 并不一定比线程更快,因为它们需要在实际线程上运行。
-
goroutine 真正的优势在于上下文切换、内存占用、创建和销毁的成本等方面。
你可能以前听说过 goroutine 调度器,但我们真正了解它的工作原理吗?它是如何将 goroutine 与线程配对的?
现在让我们一步一步地分解调度器的工作原理。
goroutine M:N 调度器模型
Go 团队真的为我们简化了并发编程,想想看:创建一个 goroutine 只需要在函数前加上 go 关键字就可以了。
go doWork()
但在这个简单的步骤背后,有一个更深层次的系统在运作。
一开始,Go 就没有简单地为我们提供线程。相反,中间有一个助手,即 goroutine 调度器,它是 Go 运行时的关键部分。
那么
M:N
这个标签是什么意思呢?
它体现了 Go 调度器在将
M
个 goroutine 映射到
N
个内核线程方面的作用,形成了
M:N
模型。操作系统线程的数量可以多于 CPU 核心数,就像 goroutine 的数量也可以多于操作系统线程一样。
在深入探讨调度器之前,让我们先区分一下经常混淆的两个概念:并发和并行。
-
并发
: 指同时处理多个任务,这些任务都在运行,但不一定在同一时间运行。
-
并行
: 指多个任务在同一时刻真正同时运行,通常使用多个 CPU 核心。
-
让我们看看 Go Scheduler 如何使用线程。
PMG 模型
在我们解开内部工作原理之前,让我们先解释一下 P、M 和 G 分别代表什么意思。
G (goroutine)
goroutine 是 Go 中最小的执行单元,类似于一个轻量级线程。
在 Go 运行时,它由一个名为
g
的 struct 表示。一旦创建,它就会被放入逻辑处理器 P 的本地可运行队列(或全局队列),之后 P 会将它分配给一个实际的内核线程(M)。
goroutine 通常存在三种主要状态:
-
Waiting
:在这个阶段,goroutine 处于静止状态,可能是由于等待某个操作(如 channel 或锁),或者是被系统调用阻塞。
-
Runnable
:goroutine 已准备就绪,但尚未开始运行,它正在等待轮到在线程(M)上运行。
-
Running
:现在 goroutine 正在线程(M)上积极执行。它将一直运行直到任务完成,除非调度器中断它或其他事物阻碍了它的运行。
goroutine 不是一次性使用后就被丢弃的。
相反,当启动一个新的 goroutine 时,Go 的运行时会从 goroutine 池中选择一个,如果池中没有,它会创建一个新的。然后,这个新的 goroutine 会加入某个 P 的可运行队列。
P(逻辑处理器)
在 Go 调度器中,当我们提到"处理器"时,指的是一个逻辑实体,而不是物理实体。
默认情况下,P 的数量设置为可用的 CPU 核心数,你可以使用 runtime.GOMAXPROCS(int)检查或更改这些处理器的数量:
runtime.GOMAXPROCS(0) // get the current allowed number of logical processors
// Output: 8 (depends on your machine)
如果你想修改 P 的数量,最好在应用程序启动时就这样做,因为如果在运行时修改,它会导致
STW
(stopTheWorld),所有操作都会暂停,直到处理器大小调整完成。
每个 P 都有自己的可运行 goroutine 列表,称为本地运行队列(Local Run Queue),最多可容纳 256 个 goroutine。
如果 P 的队列已满(256 个 goroutine),还有一个名为全局运行队列(Global Run Queue)的共享队列,不过我们稍后再讨论这个。
❝
"那么,'P'的数量真正显示了什么呢?"
它表示可以并发运行的 goroutine 数量 - 想象它们并排运行。
M(机器线程 - 操作系统线程)
一个典型的 Go 程序最多可使用 10,000 个线程。
没错,我说的是线程而不是 goroutine。如果超过这个限制,你的 Go 应用程序就有崩溃的风险。
❝
"线程是何时创建的呢?"
想象这种情况:一个 goroutine 处于可运行状态并需要一个线程。
如果所有线程都已被阻塞,可能是由于系统调用或不可抢占的操作,会发生什么?在这种情况下,调度器会介入并为该 goroutine 创建一个新线程。
(
需要注意的一点是:如果一个线程只是在进行昂贵的计算或长时间运行的任务,它不被视为陷入困境或被阻塞
)
如果你想改变默认的线程限制,可以使用
runtime/debug.SetMaxThreads()
函数,它允许你设置 Go 程序可使用的最大操作系统线程数。
另外,值得一提的是,线程会被重用,因为创建或删除线程是一个资源密集型的操作。
MPG 是如何协同工作的
让我们通过以下步骤一步步理解 M、P 和 G 是如何协同工作的。
在这里我不会深入探讨每一个细节,但在后续的文章中会更深入地探讨。如果你对此感兴趣,请关注我的公众号。
-
初始化一个 goroutine:使用 go func()命令时,Go 运行时会新建一个 goroutine 或从池中选择一个已存在的 goroutine。
-
入队排位:goroutine 会寻找一个队列来加入,如果所有逻辑处理器(P)的本地队列都已满,该 goroutine 会被放入全局队列。
-
线程配对:这就是 M 发挥作用的地方。它获取一个 P,并开始从 P 的本地队列处理 goroutine。当 M 与这个 goroutine 交互时,其关联的 P 就会被占用,无法分配给其他 M。
-
窃取行为:如果一个 P 的队列被耗尽,M 会试图从另一个 P 的队列"借用"一半可运行的 goroutine。如果失败,它会检查全局队列,然后是网络轮询器(参见下面的"窃取过程"图)。
-
资源分配:在 M 选择一个 goroutine(G)后,它会为运行这个 G 获取所需的所有资源。
❝
"如果一个线程被阻塞了怎么办?"
如果一个 goroutine 启动了一个需要一段时间的系统调用(比如读取文件),M 会一直等待。
但调度器不喜欢一直等待,它会将被阻塞的 M 从它的 P 上分离,然后将队列中另一个可运行的 goroutine 连接到一个新的或已存在的 M 上,M 再与 P 团队合作。
窃取过程
当一个线程(M)完成了它的任务,没有其他事情可做时,它不会就这样闲置。
相反,它会主动寻找更多工作,方法是查看其他处理器并接手它们一半的任务,让我们来分解一下这个过程:
-
每 61 个嘀嗒,一个 M 会检查全局可运行队列,以确保公平执行。如果在全局队列中找到了可运行的 goroutine,就停止。
-
该线程 M 现在会检查与它所在的处理器 P 相连的本地运行队列,看看有没有可运行的 goroutine 需要处理。
-
如果该线程发现它的队列为空,它就会查看全局队列,看看是否有任何等待中的任务。
-
然后,该线程会向网络轮询器询问是否有任何与网络相关的工作。
-
如果该线程在检查完网络轮询器后仍然没有找到任何任务,它就会进入主动搜索模式,我们可以将其视为自旋状态。
-
在这种状态下,该线程会尝试从其他处理器的队列中"借用"任务。
-
经过这些步骤后,如果该线程仍然没有找到任何工作,它就会停止主动搜索。
-
现在,如果有新的任务到来,并且有空闲的处理器没有正在搜索的线程,另一个线程就可以开始工作。
需要注意的一点是,全局队列实际上被检查了两次:一次是每 61 个嘀嗒检查一次以保证公平性,另一次是在本地队列为空时检查。
❝
"如果 M 已与其 P 绑定,它怎么能从其他处理器获取任务呢?M 会改变它的 P 吗?"
答案是不会。
即使 M 从另一个 P 的队列中获取任务,它也是使用原来的 P 来运行该任务。因此,尽管 M 获取了新任务,但它仍然忠于自己的 P。
❝
"为什么是 61?"
在设计算法时,尤其是哈希算法时,通常会选择素数,因为素数除了 1 和自身之外没有其他因子。
这可以减少出现模式或规律性的可能性,从而避免发生"冲突"或其他不希望出现的行为。
如果时间过短,系统可能会频繁浪费资源检查全局运行队列。如果时间过长,goroutine 可能会在执行前过度等待。
网络轮询器(Network Poller)
我们还没有太多讨论这个网络轮询器,但它出现在了窃取过程的示意图中。
与 Go 调度器一样,网络轮询器也是 Go 运行时的一个组件,负责处理与网络相关的调用(例如网络 I/O)。
让我们比较一下两种系统调用类型:
-
与网络相关的系统调用: 当一个 goroutine 执行网络 I/O 操作时,它不会阻塞当前线程,而是向网络轮询器注册。轮询器异步等待操作完成,一旦完成,该 goroutine 就可以再次变为可运行状态,并在某个线程上继续执行。
-
其他系统调用: 如果它们可能会阻塞且没有由网络轮询器处理,它们可能会导致 goroutine 将执行卸载到一个操作系统线程上。只有该特定的操作系统线程会被阻塞,Go 运行时调度器可以在其他线程上执行其他 goroutine。
在后续部分,我们将更深入地探讨抢占式调度,并分析调度器在运行过程中所采取的每一步骤。
原文:
Goroutine Scheduler Revealed: Never See Goroutines the Same Way Again
[1]
[1]
Goroutine Scheduler Revealed: Never See Goroutines the Same Way Again:
https://blog.devtrovert.com/p/goroutine-scheduler-revealed-youll