揭秘系列: Goroutine调度器

现在不要担心理解上面的图片,因为我们将从非常基础的知识开始。

Goroutines分布在线程中,由Goroutine调度器在幕后处理。根据我们之前的讨论,我们知道一些关于Goroutines的事情:

•从原始执行速度来看,Goroutines不一定比线程更快,因为它们需要一个实际的线程来运行。•Goroutines的真正优势在于上下文切换、内存占用、创建和拆除的成本等方面。

你可能之前听说过Goroutine调度器,但我们真正了解它是如何工作的吗?它是如何将Goroutines与线程配对的?

现在让我们一步一步地分解调度器的操作。

1. Goroutine的M:N调度器

Go团队为我们真正简化了并发处理,想想看:创建一个Goroutine就像在函数前面加上 go 关键字一样容易。

go 复制代码
go doWork()

但在这个简单的步骤背后,有一个更深层的系统在运作。

从一开始,Go并不是简单地提供了线程。相反,在中间有一个辅助工具,Goroutine调度器,它是Go运行时的关键部分。

1*6UyqGhkbOV7kSlRe1CD-gA.png

那么什么是M:N标签?

它表示Go调度器在将M个Goroutines映射到N个内核线程时所起的作用,形成了M:N模型。你可以拥有更多的操作系统线程,就像可以拥有更多的Goroutines一样。

在我们深入研究调度器之前,让我们澄清一下经常混淆的两个术语:并发和并行。

并发 :这是关于同时处理多个任务,它们都在运动,但不总是在同一时刻。•并行:这意味着许多任务在完全相同的时间运行,通常使用多个CPU核心。

1*30ViMAPkVySvdSDc-hI3sA.png

让我们看看Go调度器是如何与线程配合运作的。

2. PMG 模型

在我们深入研究内部工作原理之前,让我们分解一下P、M和G代表什么。

G(Goroutine)

Goroutine充当Go的最小执行单元,类似于轻量级线程。

在Go的运行时中,它由一个名为gstruct{}表示。一旦创建,它会找到一个逻辑处理器P的本地可运行队列(或G队列)中的位置,然后P将其交给一个实际的内核线程M。

Goroutines通常存在三种主要状态:

等待 :在这个阶段,Goroutine停滞不前,可能因为通道或锁之类的操作而暂停,或者可能被系统调用暂停。•可运行 :Goroutine已经准备好运行,但尚未开始运行,它正在等待轮到它在一个线程(M)上运行。•运行:现在,Goroutine正在一个线程(M)上积极执行。它会一直执行,直到任务完成,除非调度器中断它或其他原因阻止了它的执行。

1*U62eyES6_koQtsv_9jWKHw.png

Goroutines并不仅仅被使用一次然后被丢弃。

相反,当启动新的Goroutine时,Go的运行时会从Goroutine池

中选择一个,但如果找不到任何可用的Goroutine,它会创建一个新的。然后,这个新的Goroutine加入到一个P的可运行队列中。

P(逻辑处理器)

在Go调度器中,当我们提到"处理器"时,我们指的是逻辑实体,而不是物理实体。

默认情况下,P的数量设置为可用的核心数,你可以使用runtime.GOMAXPROCS(int)函数来检查或更改这些处理器的数量。

go 复制代码
runtime.GOMAXPROCS(0) // 获取当前允许的逻辑处理器数量

如果你想更改这个数量,最好是在应用程序启动时更改它,如果在运行时更改,它会导致STW(停止一切),直到重新调整处理器。

每个P都拥有自己的可运行Goroutines列表,称为本地运行队列,最多可以容纳256个Goroutines。

1*0EneA397HA1uYYeg0_HspQ.png

调度器 --- P(逻辑处理器)

如果P的队列达到了最大Coroutines数(256),那么就有一个共享队列,称为全局运行队列,但我们将稍后讨论这个。

"那么 'P' 的这个数量到底代表什么?" 它表示可以同时运行的Goroutines数量 --- 想象它们并行运行。

M(机器线程 --- 操作系统线程)

一个典型的Go程序最多可以使用1万个线程。

是的,我说的是线程,而不是Goroutines。如果超出这个限制,你可能会使你的Go应用程序崩溃。

"什么情况下会创建一个线程?" 想象一种情况:一个Goroutine处于可运行状态并需要一个线程。如果所有线程已经被阻塞,可能是因为系统调用或非抢占操作,会怎么样?在这种情况下,调度器会介入并为该Goroutine创建一个新线程。一个需要注意的事情是,如果一个线程只是忙于昂贵的计算或长时间运行的任务,它不被视为被卡住或被阻塞。如果你想更改默认线程限制,你可以使用 runtime/debug.SetMaxThreads() 函数,它允许你设置你的Go程序可以使用的操作系统线程的最大数量。此外,值得知道的是,线程是可以重复使用的,因为创建或销毁线程是消耗资源的。

3. MPG 工作原理

让我们通过项目符号逐步了解 M、P 和 G 如何一起运作。

我不会在这里深入讨论每个细节,但我将在即将发布的故事中深入探讨。如果你感兴趣,请订阅。

1*d4hu416FJtHHaJaKJFYkGg.png

Go Scheduler 的工作原理

1.初始化 goroutine :通过使用 go func() 命令,Go Runtime 要么创建一个新的 goroutine,要么从池中选择一个现有的。2.排队位置 :goroutine 寻找其在队列中的位置,如果所有逻辑处理器(P)的本地队列都满了,那么这个 goroutine 就被放入全局队列。3.线程配对 :这是 M 开始发挥作用的地方。它获取一个 P 并开始处理来自 P 本地队列的 goroutine,当 M 与这个 goroutine 交互时,与之关联的 P 就变得占用,不再可用于其他 M。4.窃取行为 :如果某个 P 的队列被耗尽,M 会尝试"借用"另一个 P 队列中一半可运行的 goroutine。如果不成功,它然后检查全局队列,然后再检查网络轮询器(请查看下面的"窃取过程"图表部分)。5.资源分配 :M 选择了一个 goroutine(G)之后,它会获取运行 G 所需的所有资源。

"那么被阻塞的线程呢?" 如果一个 goroutine 启动了需要时间的系统调用(比如读取文件),那么 M 会等待。但调度程序不喜欢某个只是坐在那里等待的线程,它会将被暂停的 M 与其 P 解除连接,并将来自队列的另一个可运行的 goroutine 与新的或现有的 M 连接起来,然后与 P 协作。

被阻塞的线程

窃取过程

当一个线程(M)完成了它的任务并没有其他事情可做时,它不会坐在那里。

相反,它积极地寻找更多工作,观察其他处理器并获取它们一半的任务,让我们来详细了解一下:

\

ewA.png

1.每 61 个时钟滴答,M 检查全局可运行队列,以确保执行的公平性。如果在全局队列中找到一个可运行的 goroutine,就停止。2.然后,线程 M 检查其本地运行队列,与其处理器 P 相关联,以查看是否有可运行的 goroutine 可以处理。3.如果线程发现它的队列是空的,那么它会查看全局队列,看看那里是否有等待处理的任务。4.然后,线程会检查网络轮询器,以查看是否有与网络相关的任务。5.如果线程在检查了网络轮询器后仍然没有找到任务,它将进入主动搜索模式,我们可以将其视为旋转状态。6.在这种状态下,线程试图从其他处理器的队列中"借用"任务。7.经过所有这些步骤后,如果线程仍然找不到工作,它将停止主动搜索。8.现在,如果有新的任务进来,而且有一个没有在搜索状态的空闲处理器,那么可以提示另一个线程开始工作。

需要注意的细节是全局队列实际上被检查了两次:每 61 个时钟滴答一次以确保公平性,如果本地队列为空,就再次检查。

"如果 M 与其 P 相关联,它怎么能从其他处理器那里获取任务呢?M 会更改其 P 吗?" 答案是不会。即使 M 从另一个 P 的队列中获取任务,它仍然使用其原始处理器来运行该任务。因此,在 M 承担新任务的同时,它仍然忠实于其处理器。
"为什么是 61?" 在设计算法时,特别是哈希算法,通常会选择质数,因为它们除了 1 和它们自己之外没有除数。这可以降低出现模式或规律的机会,从而防止"碰撞"或其他不希望出现的行为。如果太短,系统可能会浪费资源频繁检查全局运行队列。如果太长,goroutine 可能会在执行之前等待过长的时间。

网络轮询器

我们还没有详细讨论网络轮询器,但它在窃取过程图表中提到了。

与 Go Scheduler 一样,网络轮询器是 Go Runtime 的组成部分,负责处理与网络相关的调用(例如,网络 I/O)。

让我们比较两种系统调用类型:

•与网络相关的系统调用:当一个 goroutine 执行网络 I/O 操作时,它不会阻塞线程,而是会在网络轮询器中注册。轮询器会异步等待操作完成,一旦完成,goroutine 就会再次可运行,可以在一个线程上继续执行。•其他系统调用:如果它们可能会阻塞并且不由网络轮询器处理,它们可能会导致 goroutine 将其执行卸载到操作系统线程上。只有特定的操作系统线程会被阻塞,Go 运行时调度程序可以在不同线程上执行其他 goroutine。\

相关推荐
Abladol-aj1 小时前
并发和并行的基础知识
java·linux·windows
清水白石0081 小时前
从一个“支付状态不一致“的bug,看大型分布式系统的“隐藏杀机“
java·数据库·bug
吾日三省吾码6 小时前
JVM 性能调优
java
弗拉唐7 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi778 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
少说多做3438 小时前
Android 不同情况下使用 runOnUiThread
android·java
知兀8 小时前
Java的方法、基本和引用数据类型
java·笔记·黑马程序员
蓝黑20209 小时前
IntelliJ IDEA常用快捷键
java·ide·intellij-idea
Ysjt | 深9 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++