当 Goroutines 切换时,CPU 上下文会发生什么?

5
如果我正确理解goroutine如何在系统线程上运行-它们一个接一个地从队列中运行。但这是否意味着每个goroutine都会将其上下文加载/卸载到CPU?如果是的话,系统线程和goroutine之间有什么区别?
最重要的问题是上下文切换的时间成本。这正确吗?
检测哪些数据被哪个goroutine请求的机制是什么?例如:我从goroutine A发送请求到DB,并且不等待响应,同时发生切换到下一个goroutine。系统如何理解请求来自A而不是B或C?

“最重要的问题”是指什么?是“上下文切换的时间成本”吗?Goroutines只是一个小堆栈,所以切换goroutines只是交换一些指针;没有完整的“上下文切换”。我真的无法理解你在最后一段中的误解出在哪里。 - JimB
哪些寄存器被goroutines使用?例如,任务/ goroutines A正在向数据库发送请求。 响应需要一些时间,并且系统已经运行了其他任务。 但是当数据到达时,系统如何将其发送到正确的(A)任务? 它如何理解它们是为它而来的? - Dmitry Bubnenkov
“DB请求”与goroutine的抽象级别不同。所有函数调用都是同步的,因此返回值只是在goroutine内部向上移动堆栈;在这个意义上,“DB请求”并没有什么不同。 - JimB
2个回答

15

Go协程、内存和操作系统线程

Go拥有一个按需增长的分段栈。Go运行时负责调度,而不是操作系统(OS)。运行时将协程复用到相对较少的真实OS线程中。

协程切换成本

协程被合作式地调度,当发生切换时,只需要保存/恢复三个寄存器 - 程序计数器(PC)、栈指针(SP)和DX。从OS的视角来看,Go程序就像是一个事件驱动的程序。

协程和CPU

你不能直接控制运行时将创建多少个线程。可以通过使用runtime.GOMAXPROCS(n)函数来设置程序使用的处理器核心数。

程序计数器

以及一个完全不同的故事

在计算机中,程序是计算机执行的一组有序操作。指令是程序向计算机处理器发出的命令。在计算机内部,地址是内存或存储器中的特定位置。程序计数器寄存器是处理器使用的一小组数据保持位置之一。

这是有关程序如何工作并互相通信的另一个故事,与协程主题没有直接相关性。

来源:


5

Gs, Ms 和 Ps

"G" 表示 Go 协程 (goroutine),类型为 g。当一个协程退出时,它的 G 对象会被返回到空闲的 G 池中并可以被重用于其他协程。

"M" 是操作系统线程,可以执行用户 Go 代码、运行时代码、系统调用或者处于空闲状态。类型为 m。由于任意数量的线程可能受阻于系统调用,因此可以同时存在多个 M。

最后,“P” 表示执行用户 Go 代码所需的资源,例如调度程序和内存分配器状态。类型为 p。一共有 GOMAXPROCS 个 P。可以将 P 视为操作系统调度程序中的 CPU,而 p 类型的内容则像是每个 CPU 的状态。这是需要高效划分状态但不需要每个线程或每个协程特定的一个好位置。

调度程序的任务是匹配一个 G(要执行的代码)、一个 M(在哪里执行)和一个 P(正确的和所需的资源来执行)。例如,当一个 M 停止执行用户 Go 代码(例如进入系统调用)时,它会将其对应的 P 返回到空闲的 P 池中。为了继续执行用户 Go 代码(例如从系统调用返回时),它必须从空闲池中获取一个 P。

所有 g、m 和 p 对象都是堆分配的,但从不释放,因此它们的内存保持类型稳定。结果,运行时可以在调度程序的深处避免写屏障。

用户栈和系统栈

每个非死亡 G 都有一个与之关联的用户栈,在其中执行用户 Go 代码。用户栈开始很小(例如 2K),并动态增长或缩小。

每个 M 都有一个与之关联的系统栈(也称为 M 的 "g0" 栈,因为它被实现为一个存根 G),以及在 Unix 平台上还有一个信号栈(也称为 M 的 "gsignal" 栈)。系统栈和信号栈不会增长,但足够大以执行运行时和 cgo 代码(在纯 Go 二进制文件中为 8K,在 cgo 二进制文件中由系统分配)。

运行时代码通常使用 systemstack、mcall 或 asmcgocall 暂时切换到系统栈,以执行不能被抢占、不能增长用户栈或者切换用户协程的任务。在系统栈上运行的代码隐式地不能被抢占,并且垃圾收集器不扫描系统栈。在系统栈上运行时,当前的用户栈不用于执行。

参考:https://github.com/golang/go/blob/master/src/runtime/HACKING.md


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接