优秀的编程知识分享平台

网站首页 > 技术文章 正文

Go GMP调度流程详介绍(go 调度机制)

nanyue 2024-09-01 20:32:25 技术文章 5 ℃

GMP流程:

我们通过 go func()来创建一个goroutine;

有两个存储goroutine的队列,一个是局部调度器P的local queue(当p绑定的时候m的时候,可以无锁分配内存和无锁访问任务队列)、一个是全局调度器数据模型schedt的global queue。新创建的goroutine会先保存在local queue,如果local queue已经满了就会保存在全局的global queue;

goroutine只能运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的local queue弹出一个Runable状态的goroutine来执行,如果P的local queue为空,就会执行work stealing;

一个M调度goroutine执行的过程是一个loop;

当M执行某一个goroutine时候如果发生了syscall或则其余阻塞操作,M会阻塞,如果当前有一些G在执行,runtime会把这个线程M从P中摘除(detach),然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P;

当系统调用结束时候,这个goroutine会尝试获取一个空闲的P执行,并放入到这个P的local queue。如果获取不到P,那么这个线程M会park它自己(休眠), 加入到空闲线程中,然后这个goroutine会被放入schedt的global queue。

GPM简介(具体结构见下文数据结构):

M(Machine)

OS线程抽象,代表着真正执行计算的资源, 每一个goroutine实际上就是在M中执行,M的数量目前最多10000个。可以调用标准库代码包runtime/debug中的SetMaxThreads函数控制最大个数。

M与P绑定,调度循环的执行G并发任务。M通过修改寄存器,将执行栈指向G自带的栈内存,并在此空间内分配堆栈帧,执行任务函数。中途切换时,将寄存器值保存回G的空间即可维持状态,任何M都可以恢复。线程只负责执行,不保存状态,这是并发任务跨线程调度,实现多路复用的根本所在

P(Processor)

分配程序执行的上下文环境, 数量<=内核数量, 即同时能够并行执行的G的数量,相对于G而言, P的角色相当于CPU,每个工作线程必须绑定一个p才能正常运行,否则只能休眠,等待空闲的p,然后被唤醒。

p为线程提供了执行资源,如对象分配内存,本地任务队列等。线程独享p资源,可以无锁操作。G程序代码中的每一次使用关键字go执行函数其实都生成了一个G,并将之加入到本地的G队列中, 之后M会生成G执行的上下文也就是绑定P来执行函数。

p控制了程序的并行度,如果还有一个p,那么同时只能执行一个任务,可以通过GOMAXPROCS设置p的个数。在Go1.5之后GOMAXPROCS被默认设置可用的核数,而之前则默认为1。

G(Goroutine)

维护者goroutine需要的栈、程序计数器以及它所在的M等信息。基本上进程的一切都在g上运行。g并非执行体,它仅仅保存并发任务状态,为人物提供所需要的栈空间。g创建成功后放在p的本地队列或者全局队列,等待调度

基本数据结构

下文介绍调度其中会使用到的部分基本数据结构,源码中的数据结构在runtime包中的runtime2.go中

stack

stack 描述了Go执行堆栈。堆栈的边界正好是 [lo,hi),两边都没有隐式数据结构。

type stack struct {
lo uintptr
hi uintptr
}

gobuf

g 的运行现场,创建时初始化,用来保存g当前的运行状况,g被抢占是会保存当前现场,下次调度的时候从这里恢复

type gobuf struct {
sp uintptr // sp 寄存器
pc uintptr // pc 寄存器
g guintptr // g 指针
ctxt unsafe.Pointer // 这个似乎是用来辅助 gc 的
ret sys.Uintreg
lr uintptr // 这是在 arm 上用的寄存器,不用关心
bp uintptr // 开启 GOEXPERIMENT=framepointer,才会有这个
}

G里面比较重要的成员如下

stack: 当前g使用的栈空间, 有lo和hi两个成员

stackguard0: 检查栈空间是否足够的值, 低于这个值会扩张栈, 0是go代码使用的

stackguard1: 检查栈空间是否足够的值, 低于这个值会扩张栈, 1是原生代码使用的

m: 当前g对应的m

sched: g的调度数据, 当g中断时会保存当前的pc和rsp等值到这里, 恢复运行时会使用这里的值

atomicstatus: g的当前状态

schedlink: 下一个g, 当g在链表结构中会使用

preempt: g是否被抢占中

lockedm: g是否要求要回到这个M执行, 有的时候g中断了恢复会要求使用原来的M执行

G的状态:

空闲中(_Gidle): 表示G刚刚新建, 仍未初始化

待运行(_Grunnable): 表示G在运行队列中, 等待M取出并运行

运行中(_Grunning): 表示M正在运行这个G, 这时候M会拥有一个P

系统调用中(_Gsyscall): 表示M正在运行这个G发起的系统调用, 这时候M并不拥有P

等待中(_Gwaiting): 表示G在等待某些条件完成, 这时候G不在运行也不在运行队列中(可能在channel的等待队列中)

已中止(_Gdead): 表示G未被使用, 可能已执行完毕(并在freelist中等待下次复用)

栈复制中(_Gcopystack): 表示G正在获取一个新的栈空间并把原来的内容复制过去(用于防止GC扫描)

sudog

当 g 遇到阻塞,或需要等待的场景时,会被打包成 sudog 这样一个结构。一个 g 可能被打包为多个 sudog 分别挂在不同的等待队列上:

m

线程在 runtime 中的结构,对应一个 pthread,pthread 也会对应唯一的内核线程(task_struct):

M里面比较重要的成员如下

g0: 用于调度的特殊g, 调度和执行系统调用时会切换到这个g

curg: 当前运行的gp: 当前拥有的P

nextp: 唤醒M时, M会拥有这个P

park: M休眠时使用的信号量, 唤醒M时会通过它唤醒

schedlink: 下一个m, 当m在链表结构中会使用

mcache: 分配内存时使用的本地分配器, 和p.mcache一样(拥有P时会复制过来)

lockedg: lockedm的对应值

M并没有像G和P一样的状态标记, 但可以认为一个M有以下的状态:

自旋中(spinning): M正在从运行队列获取G, 这时候M会拥有一个P

执行go代码中: M正在执行go代码, 这时候M会拥有一个P

执行原生代码中: M正在执行原生代码或者阻塞的syscall, 这时M并不拥有P

休眠中: M发现无待运行的G时会进入休眠, 并添加到空闲M链表中, 这时M并不拥有P

M可以运行两种代码:

go代码, 即goroutine, M运行go代码需要一个P(运行常规的g)

原生代码, 例如阻塞的syscall, M运行原生代码不需要P(运行在m的go上)

自旋中(spinning)这个状态非常重要, 是否需要唤醒或者创建新的M取决于当前自旋中的M的数量.通常创建一个M的原因是由于没有足够的M来关联P并运行其中可运行的G。而且运行时系统执行系统监控的时候,或者GC的时候也会创建M。

p

抽象数据结构,可以认为是 processor 的抽象,代表了任务执行时的上下文,m 必须获得 p 才能执行。

P里面比较重要的成员如下

status: p的当前状态

link: 下一个p, 当p在链表结构中会使用

m: 拥有这个P的M

mcache: 分配内存时使用的本地分配器

runqhead: 本地运行队列的出队序号

runqtail: 本地运行队列的入队序号

runq: 本地运行队列的数组, 可以保存256个G

gfree: G的自由列表, 保存变为_Gdead后可以复用的G实例

gcBgMarkWorker: 后台GC的worker函数, 如果它存在M会优先执行它

gcw: GC的本地工作队列

P的状态

空闲中(_Pidle): 当M发现无待运行的G时会进入休眠, 这时M拥有的P会变为空闲并加到空闲P链表中

运行中(_Prunning): 当M拥有了一个P后, 这个P的状态就会变为运行中, M运行G会使用这个P中的资源

系统调用中(_Psyscall): 当go调用原生代码, 原生代码又反过来调用go代码时, 使用的P会变为此状态

GC停止中(_Pgcstop): 当gc停止了整个程序(STW)时, P会变为此状态

已中止(_Pdead): 当P的数量在运行时改变, 且数量减少时多余的P会变为此状态

schedt

全局调度器,全局只有一个 schedt 类型的实例:

Tags:

最近发表
标签列表