学习
分享
Learning: Golang GMP
输入“/”快速插入内容
Learning: Golang GMP
飞书用户409
5月11日修改
golang的一大特点就是用户态线程(协程)是一等公民(即由语言的关键字创建,而不是某个标准库),我们来看下用户态线程的一些关键细节。
线程逻辑关系
我们知道,在不同层面上都有线程的说法,比如硬件线程,操作系统线程,用户态线程。
•
硬件线程:由硬件提供的线程,其实就是一段指令序列实际执行的一条通道。一般来说,一个cpu核心就是一个硬件线程,同一时间只能处理一段硬件指令。intel有超线程技术,可以同一时间处理两条指令序列,也就是有两个硬件线程,也就是常说的比如四核八线程。
•
操作系统线程:一般来说,我们会有多条并行执行的进程,每个进程可能有多个线程,这里的线程一般就是指操作系统线程,于是,一台服务器可能有成千上万个操作系统线程。这个操作系统线程将会被操作系统调度,在合适的时间放在硬件线程上执行,操作系统为我们屏蔽了与硬件的交互,我们只需要和操作系统线程打交道即可。操作系统线程是调度和执行的最小单位。
•
用户态线程:指完全运行在用户态程序的线程,内核/操作系统不感知这种线程,即在操作系统线程里,又有多个可并行执行的指令序列,由用户态程序自己来控制在该操作系统线程上执行哪段指令序列
这样的话,很显然,我们获得了 k:n:m 的一个关系
GMP
Goroutine(用户态线程)-machine(操作系统线程)-processor(处理器)
1.
每个goroutine被创建出来后,首先加入allg进行追踪,随后被加入当前P的本地runq队列
2.
如果存在自旋(spin)状态的M,M会轮询到空闲的P,执行绑定和上下文恢复以及运行g操作
a.
自旋(spin)状态指通过for循环不断检查是否有空闲的P和待执行的G,自旋几十微秒后睡眠
3.
如果都是空闲(idle)状态的M, 由sysmon线程负责唤醒或创建一个M,并会传入一个目标P, 被唤醒后的M会执行绑定P的操作,并恢复上下文以及运行g
a.
空闲(idle)状态指线程睡眠,需要由sysmon线程唤醒。睡眠用的是futex的指令:
futex(FUTEX_WAIT) futex(FUTEX_WAKE)
, 直接在用户态阻塞.
如何实现用户态线程
1.
用户态线程切换逻辑:
a.
在go中通过汇编编写,在汇编中,会将当前的程序计数器、寄存器状态保存在goroutine对应结构体的某个字段上,然后跳转到调度器的schedule函数,执行调度逻辑,调度逻辑会将其他goroutine指令调度到该操作系统线程上执行。恢复执行同理,通过汇编设置程序计数器和寄存器的状态。
2.
何时切换:
a.
当陷入阻塞时,或者时间片耗尽(插入检查点或者抢占调度),或者主动触发调度(runtime.GoSched()),都会促使调度
b.
时间片耗尽触发调度由两种方式:
i.
go1.14 之前基于协作,也就是完全由goroutine运行过程中自己发现自己是否需要让出os thread,原理就是编译时在代码片段中注入一些检查点,运行到检查点时主动check一下是否需要让出,这依赖于编译时的注入能力,有可能某些边界case没注入成功,而且也无法预估指令的运行时长,时间片容易多占。
ii.
go1.14 引入抢占调度(和协作调度一起工作),会另起一个sysmon线程,用于监控各个正在运行的goroutine的时间片,如果超出,会发送信号给对应的操作系统线程,执行信号处理函数让出os thread
1.
sysmon线程负责goroutine抢占、gc操作、netpoll等
2.
sysmon线程绕过了GMP调度,直接创建一个M并运行,无需绑定P
3.
为何能切换?
a.
时间片耗尽的主动检查和信号处理、或者主动触发调度时,都好理解可以触发切换,但是io阻塞时,是如何切换的呢?
b.
实际上,编译时会在可能阻塞的系统调用代码前后插入entersyscall 和 exitsyscall
i.
Entersyscall: 保存当前执行上下文到G(goroutine对应的结构体,runtime.g)上, M与P解绑,随后M陷入阻塞(也就是通常的操作系统线程阻塞),此时G/M不会被添加到什么队列里,G就挂在M上。
1.
要找到G/M的话,可以从全局的 allg/allm 数组里找
ii.
Exitsyscall: 为当前阻塞可运行的M寻找一个P(优先之前的P), 如果找到了,恢复运行(加载g的上下文)。如果找不到,将g放到全局队列中。M进入阻塞或销毁。
一言以蔽之
go注入了一些代码来拦截传统的流程,最终完成了用户态线程调度