Golang GMP解读

概念梳理

1. 1 线程

通常语义中的线程,指的是内核级线程,核心点如下:

  1. 是操作系统最小调度单元;
  2. 创建、销毁、调度交由内核完成,cpu 需完成用户态与内核态间的切换;
  3. 可充分利用多核,实现并行.

1.2 协程

协程又称为用户级线程核心点如下:

  1. 与线程存在映射关系,为 M:1,即多个协程对应一个线程
  2. 创建、销毁、调度在用户态完成,对内核透明,所以更轻;
  3. 从属同一个内核级线程,无法并行;一个协程阻塞会导致从属同一线程的所有协程无法执行.

1.3 Goroutine

Goroutine,经 Golang 优化后的特殊“协程”,核心点如下:

  1. 与线程存在映射关系,为 M:N,即 goroutine 既有协程M对1的特性,也存在1对1的可能,甚至1对N
  2. 创建、销毁、调度在用户态完成,对内核透明,足够轻便;
  3. 可利用多个线程,实现并行;
  4. 通过调度器的斡旋,实现和线程间的动态绑定和灵活调度;
  5. 栈空间大小可动态扩缩,因地制宜.

1.4 三种模型的能力对比

模型依赖内核可并行可应对阻塞栈可动态扩缩
线程X
协程XXXX
goroutineX

goroutine更像是一个博采众长的存在。实际上,“灵活调度” 一词概括得实在过于简要,Golang 在调度 goroutine 时,针对“如何减少加锁行为”,“如何避免资源不均”等问题都给出了精彩的解决方案,这一切都得益于经典的 “gmp” 模型

GMP模型

gmp = goroutine + machine + processor (+ 一套有机组合的机制),下面先单独拆出每个组件进行介绍,最后再总览全局,对 gmp 进行总述

2.1 g(goroutine)

  1. g 即goroutine,是 golang 中对协程的抽象;
  2. g 有自己的运行栈、状态、以及执行的任务函数(用户通过 go func 指定);
  3. g 需要绑定到 p 才能执行,在 g 的视角中,p 就是它的 cpu.

2.2 p(processor)

  1. p 即 processor ,是golang中的调度器
  2. p 是 gmp 的中枢,借由 p 承上启下,实现 g 和 m 之间的动态有机结合
  3. 对于 g 而言,p 是其cpu,g 只有被 p 调度才得以执行
  4. 对于 m 而言,p 是其执行代理,为其提供必要信息的同时(可执行的 g,内存分配情况等),并隐藏了复杂的调度细节
  5. p 的数量决定了 g 最大的并行数量。可以由用户通过 GoMaxProcs 设置(但是超过了CPU的核心数则无意义了)

2.3 m(machine)

  1. m 即 machine ,是golang中线程的抽象
  2. m 不直接执行 g,而是先和 p 绑定,由其代理实现
  3. 借由 p 的存在,m 无需和 g 绑死,也无需记录 g 的状态信息,因此 g 在全生命周期可以实现跨 m 执行

2.4 GMP(线程-- 使用调度器 --> 使用协程 goroutine)

GMP宏观模型

  1. M 是线程的抽象;G 是 goroutine;P 是承上启下的调度器;
  2. M调度G前,需要和P绑定;
  3. 全局有多个M和多个P,但同时并行的G的最大数量等于P的数量;
  4. G的存放队列有三类:P的本地队列;全局队列;和wait队列(图中未展示,为io阻塞就绪态goroutine队列);
  5. M调度G时,优先取P本地队列,其次取全局队列,最后取wait队列;这样的好处是,取本地队列时,可以接近于无锁化,减少全局锁竞争;
  6. 为防止不同P的闲忙差异过大,设立work-stealing机制,本地队列为空的P可以尝试从其他P本地队列偷取一半的G补充到自身队列.

核心数据结构

gmp 数据结构定义为 runtime/runtime2.go 文件中

3.1 g

type g struct {// ...// m:在 p 的代理,负责执行当前 g 的 m;m         *m      // ...sched     gobuf// ...
}
type gobuf struct {sp   uintptrpc   uintptrret  uintptrbp   uintptr // for framepointer-enabled architectures
}
  1. m:在 p 的代理,负责执行当前 g 的 m;
  2. sched.sp:保存 CPU 的 rsp 寄存器的值,指向函数调用栈栈顶;
  3. sched.pc:保存 CPU 的 rip 寄存器的值,指向程序下一条执行指令的地址;
  4. sched.ret:保存系统调用的返回值;
  5. sched.bp:保存 CPU 的 rbp 寄存器的值,存储函数栈帧的起始位置.
g 的生命周期

生命周期

const(_Gidle = itoa // 0_Grunnable // 1_Grunning // 2_Gsyscall // 3_Gwaiting // 4_Gdead // 6_Gcopystack // 8_Gpreempted // 9
)
  1. _Gidle 值为 0,为协程开始创建时的状态,此时尚未初始化完成;
  2. _Grunnable 值 为 1,协程在待执行队列中,等待被执行;
  3. _Grunning 值为 2,协程正在执行,同一时刻一个 p 中只有一个 g 处于此状态;
  4. _Gsyscall 值为 3,协程正在执行系统调用;
  5. _Gwaiting 值为 4,协程处于挂起态,需要等待被唤醒. gc、channel 通信或者锁操作时经常会进入这种状态;
  6. _Gdead 值为 6,协程刚初始化完成或者已经被销毁,会处于此状态;
  7. _Gcopystack 值为 8,协程正在栈扩容流程中;
  8. _Greempted 值为 9,协程被抢占后的状态.

3.2 m

type m struct {g0      *g     // goroutine with scheduling stack// ...tls           [tlsSlots]uintptr // thread-local storage (for x86 extern register)// ...
}
  1. g0:一类特殊的调度协程,不用于执行用户函数,负责执行 g 之间的切换调度. 与 m 的关系为 1:1;
  2. tls:thread-local storage,线程本地存储,存储内容只对当前线程可见. 线程本地存储的是 m.tls 的地址,m.tls[0] 存储的是当前运行的 g,因此线程可以通过 g 找到当前的 m、p、g0 等信息.

3.3 p

type p struct {// ...runqhead uint32runqtail uint32runq     [256]guintptrrunnext guintptr// ...
}
  1. runq:本地 goroutine 队列,最大长度伟大256
  2. runqhead:队列头部
  3. runqtail:队列尾部
  4. runnext:下一个可执行的 goroutine

3.4 schedt

sched 是全局队列的封装

type schedt struct {// ...lock mutex// ...runq     gQueuerunqsize int32// ...
}
  1. lock 操作全局对列的锁
  2. runq 全局 goroutine 队列
  3. runqsize 全局队列的长度

调度流程解析

4.1 两种 g 的转换

即 普通任务 g 和调度查找任务 g0 之间的转换
goroutine 的类型可以分为两类:

  1. 负责调度普通 g 的 g0,执行固定的调度流程,与 m 的关系为一对一;
  2. 负责执行用户函数的普通 g.
    m 通过 p 调度执行的 goroutine 永远在普通 g 和 g0 之间进行切换,当 g0 找到可执行的 g 时,会调用 gogo 方法,调度 g 执行用户定义的任务;当 g 需要主动让渡或被动调度时,会触发 mcall 方法,将执行权重新交还给 g0.
    gogo 和 mcall 可以理解为对偶关系,其定义位于 runtime/stubs.go 文件中.
func gogo(buf *gobuf)
// ...
func mcall(fn func(*g))

4.2 调度类型

通常,调度指的是由 g0 按照特定策略找到下一个可执行 g 的过程. 而本小节谈及的调度类型是广义上的“调度”,指的是调度器 p 实现从执行一个 g 切换到另一个 g 的过程.

这种广义“调度”可分为几种类型:

  1. 主动调度
    一种用户主动执行让渡的方式,主要方式是,用户在执行代码中调用了 runtime.Gosched 方法,此时当前 g 会当让出执行权,主动进行队列等待下次被调度执行.
    代码位于 runtime/proc.go
func Gosched() {checkTimeouts()mcall(gosched_m)
}
  1. 被动调度
    因当前不满足某种执行条件,g 可能会陷入阻塞态无法被调度,直到关注的条件达成后,g才从阻塞中被唤醒,重新进入可执行队列等待被调度.
    常见的被动调度触发方式为因 channel 操作或互斥锁操作陷入阻塞等操作,底层会走进 gopark 方法(例如http的IO多路复用,epoll方式使用的就是gopark来进行挂起操作)
    代码位于 runtime/proc.go
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {// ...mcall(park_m)
}

通常 goready 与 gopark 成对出现,能够将 g 从阻塞状态恢复过来的,重新进入等待执行的状态
源码位于 runtime/proc.go

func goready(gp *g, traceskip int) {systemstack(func() {ready(gp, traceskip, true)})
}
  1. 正常调度
    g 中的任务执行完后,g0 会将当前 g 置于死亡状态,发起新一轮的调度

  2. 抢占调度:
    如果 g 执行系统调度时间过长,超过了指定的市场,且全局的 p 资源比较紧缺,此时将 p 和 g 解绑,抢占出来用于其他 g 调度。等 g 完成系统调用后,会重新进入可执行队列中等待被调度
    但是跟前三种调度方式不同的是,其余三个调度方式都是在 m 下的 g0 完成的,抢占调度则不同
    因为发起系统调度时需要打破用户态的边界进入内核,此时 m 也会因系统调用而陷入僵直,无法主动完成抢占调度的行为
    所以Golang进程会有一个全局监控协程 monitor g 的存在,这个 g 会越过 p 直接跟 m 进行绑定,不断轮询对所有的 p 的执行状况进行监控,倘若发现满足抢占调度的条件,则从第三方角度出手干预。主动发起抢占调度动作

宏观调度流程串联

调度流程

  1. 以 g0 -> g -> g0 的一轮循环为例进行串联
  2. g0 执行 schedule() 函数,寻找到用于执行的 g
  3. g0 执行 execute() 方法,更新当前 g、p 的状态信息,并调用 gogo 方法,将执行权交给 g
  4. g 因主动让渡(goshce_m())、被动调度( park_m() )、正常结束( goexit0() )等原因,调用 m_call 函数,执行权重新回到 g0手中
  5. g0 执行 schedule() 函数,开启新一轮的循环

解析 schedule() 搜索可执行 g 的函数

调度流程的主干方法是位于 runtime/proc.go 中的 schedule 函数,此时的执行权位于 g0 手中:

func schedule() {// ...gp, inheritTime, tryWakeP := findRunnable() // blocks until work is available// ...execute(gp, inheritTime)
}
findRunable()

调度流程中,一个非常核心的步骤,就是为 m 寻找到下一个执行的 g,这部分内容位于 runtime/proc.go 的 findRunnable 方法中:

func findRunnable() (gp *g, inheritTime, tryWakeP bool) {_g_ := getg()top:_p_ := _g_.m.p.ptr()// ...// 判断执行查找到 61 次没有if _p_.schedtick%61 == 0 && sched.runqsize > 0 {// 加锁向全局队列进行查找lock(&sched.lock)gp = globrunqget(_p_, 1)// 释放锁unlock(&sched.lock)if gp != nil {// 返回可执行的 greturn gp, false, false}}// ...// 尝试从 p 本地队列中进行查找if gp, inheritTime := runqget(_p_); gp != nil {return gp, inheritTime, false}// ...// 判断全局队列长度,尝试从全局队列中进行查找if sched.runqsize != 0 {lock(&sched.lock)gp := globrunqget(_p_, 0)unlock(&sched.lock)if gp != nil {return gp, false, false}}// 尝试获取就绪的网络协议 --> 向 epoll 就绪队列中进行查找if netpollinited() && atomic.Load(&netpollWaiters) > 0 && atomic.Load64(&sched.lastpoll) != 0 {if list := netpoll(0); !list.empty() { // non-blockinggp := list.pop()injectglist(&list)casgstatus(gp, _Gwaiting, _Grunnable)return gp, false, false}}// ...// 尝试从其余的 p 中偷取一半的 gprocs := uint32(gomaxprocs)if _g_.m.spinning || 2*atomic.Load(&sched.nmspinning) < procs-atomic.Load(&sched.npidle) {if !_g_.m.spinning {_g_.m.spinning = trueatomic.Xadd(&sched.nmspinning, 1)}gp, inheritTime, tnow, w, newWork := stealWork(now)now = tnowif gp != nil {// Successfully stole.return gp, inheritTime, false}if newWork {// There may be new timer or GC work; restart to// discover.goto top}if w != 0 && (pollUntil == 0 || w < pollUntil) {// Earlier timer to wait for.pollUntil = w}}

调度流程如图:
g0执行流程

  1. p 每执行 61 次调度,会从全局队列中获取一个 goroutine 进行执行,并将一个全局队列中的 goroutine 填充到当前 p 的本地队列中.
 if _p_.schedtick%61 == 0 && sched.runqsize > 0 {lock(&sched.lock)gp = globrunqget(_p_, 1)unlock(&sched.lock)if gp != nil {return gp, false, false}}
// 除了查找流程外还会将全局队列中的 g 转移到本地 p
func globrunqget(_p_ *p, max int32) *g {if sched.runqsize == 0 {return nil}// 判断 全局队列长度/p 的数量 + 1 == 每个p可以分到的g的个数n := sched.runqsize/gomaxprocs + 1if n > sched.runqsize {// 全局队列只有 1 个,则直接提取一个n = sched.runqsize}// 传参 max 最大获取个数,如果 n > max 则只获取 max 个if max > 0 && n > max {n = max}// 如果获取个数超过了本地队列的一半,需要考虑能不能存的下if n > int32(len(_p_.runq))/2 {n = int32(len(_p_.runq)) / 2}// 将全局队列的长度减去获取到的 g 个数sched.runqsize -= n// 全局队列循环弹出 ggp := sched.runq.pop()n--for ; n > 0; n-- {gp1 := sched.runq.pop()// 并将多余的 g 存储到 p 本地队列中runqput(_p_, gp1, false)}return gp// 本地队列存储全局队列 g 的方法
func runqput(_p_ *p, gp *g, next bool) {// ...
retry:// 获取本地队列头节点,同时对本地队列加锁h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with consumers// 获取尾节点t := _p_.runqtail// 如果尾节点减去头节点 小于本地队列长度 == 本地队列未满if t-h < uint32(len(_p_.runq)) {// 直接将 g 插入 队列中_p_.runq[t%uint32(len(_p_.runq))].set(gp)// 将尾节点索引 + 1,并释放队列atomic.StoreRel(&_p_.runqtail, t+1) // store-release, makes the item available for consumptionreturn}// 本地队列满了if runqputslow(_p_, gp, h, t) {return}// the queue is not full, now the put above must succeedgoto retry// 本地队列满了,将获取本地队列的一半放入到全局队列中,帮助本地队列减少压力
func runqputslow(_p_ *p, gp *g, h, t uint32) bool {// 创建本地队列一半 + 1 的数组var batch [len(_p_.runq)/2 + 1]*g// First, grab a batch from local queue.n := t - h// 本地队列现有长度的一半n = n / 2// ...// for 循环放置for i := uint32(0); i < n; i++ {batch[i] = _p_.runq[(h+i)%uint32(len(_p_.runq))].ptr()}// 释放 p 的存储if !atomic.CasRel(&_p_.runqhead, h, h+n) { // cas-release, commits consumereturn false}// 将新获取到的gp也存储全局队列中batch[n] = gp// Link the goroutines.for i := uint32(0); i < n; i++ {// for循环将本地队列提取到的 g 转成链表batch[i].schedlink.set(batch[i+1])}var q gQueue// 设置头尾节点q.head.set(batch[0])q.tail.set(batch[n])// Now put the batch on global queue.lock(&sched.lock)globrunqputbatch(&q, int32(n+1))unlock(&sched.lock)return true
  1. 尝试从 p 本地队列中获取一个可执行的 goroutine,核心逻辑位于 runqget 方法中:

需要注意,虽然本地队列是属于 p 独有的,但是由于 work-stealing 机制的存在,其他 p 可能会前来执行窃取动作,因此操作仍需加锁.
但是,由于窃取动作发生的频率不会太高,因此当前 p 取得锁的成功率是很高的,因此可以说p 的本地队列是接近于无锁化,但没有达到真正意义的无锁.

 if gp, inheritTime := runqget(_p_); gp != nil {return gp, inheritTime, false}func runqget(_p_ *p) (gp *g, inheritTime bool) {// 如果当前 runnext 为非空 则直接返回下一个 runnext 即可if next != 0 && _p_.runnext.cas(next, 0) {return next.ptr(), true}for {// 加锁并获取头尾节点 ==> 虽然本地队列是 p 独有的,但是存在偷 g 的机制,所以还是需要加锁h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with other consumerst := _p_.runqtail// 如果头节点等于尾节点,则表示 p 为空if t == h {return nil, false}// g 存在则取头节点并返回,将头节点设置为下一个 并释放锁gp := _p_.runq[h%uint32(len(_p_.runq))].ptr()if atomic.CasRel(&_p_.runqhead, h, h+1) { // cas-release, commits consumereturn gp, false}}
  1. 倘若本地队列没有可执行的 g,会从全局队列中获取:
if sched.runqsize != 0 {// 加锁lock(&sched.lock)// 获取的首节点,不向 p 中存储节点gp := globrunqget(_p_, 0)// 释放锁unlock(&sched.lock)if gp != nil {return gp, false, false}}
  1. 倘若本地队列和全局队列都没有 g,则会获取准备就绪的网络协程:

需要注意的是,刚获取网络协程时,g 的状态是处于 waiting 的,因此需要先更新为 runnable 状态.

 if netpollinited() && atomic.Load(&netpollWaiters) > 0 && atomic.Load64(&sched.lastpoll) != 0 {if list := netpoll(0); !list.empty() { // non-blockinggp := list.pop()injectglist(&list)// 状态更新casgstatus(gp, _Gwaiting, _Grunnable)return gp, false, false}}
  1. work-stealing: 从其他 p 中偷取 g.
func stealWork(now int64) (gp *g, inheritTime bool, rnow, pollUntil int64, newWork bool) {pp := getg().m.p.ptr()ranTimer := false// 偷取操作最多只遍历 4 次 p 队列const stealTries = 4for i := 0; i < stealTries; i++ {stealTimersOrRunNextG := i == stealTries-1// 为保证窃取行为的公平性,遍历的起点是随机的for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() {// ...}}return nil, false, now, pollUntil, ranTime// 偷取操作
func runqgrab(_p_ *p, batch *[256]guintptr, batchHead uint32, stealRunNextG bool) uint32 {for {// 因为存在 p 也获取头节点的可能,需要加锁h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with other consumerst := atomic.LoadAcq(&_p_.runqtail) // load-acquire, synchronize with the producer// 获取长度的一半n := t - hn = n - n/2// 如果长度为 0 if n == 0 {// 是否是最后一次遍历if stealRunNextG {// Try to steal from _p_.runnext.// 查看是否有下一个要执行的 gif next := _p_.runnext; next != 0 {// 查询 p 是否允许偷取if _p_.status == _Prunning {// 等待一段执行时间if GOOS != "windows" && GOOS != "openbsd" && GOOS != "netbsd" {usleep(3)} else {osyield()}}// 等待期间已经完成执行则退出if !_p_.runnext.cas(next, 0) {continue}// 不然就偷取batch[batchHead%uint32(len(batch))] = nextreturn 1}}return 0}// 偷取一半长度大于自身的一半,退出if n > uint32(len(_p_.runq)/2) { // read inconsistent h and tcontinue}// for循环的获取for i := uint32(0); i < n; i++ {g := _p_.runq[(h+i)%uint32(len(_p_.runq))]batch[(batchHead+i)%uint32(len(batch))] = g}// 释放锁 并 改变头节点if atomic.CasRel(&_p_.runqhead, h, h+n) { // cas-release, commits consumereturn n}}
}

解析执行 g 函数execute

当 g0 为 m 寻找到可执行的 g 之后,接下来就开始执行 g. 这部分内容位于 runtime/proc.go 的 execute 方法中:

func execute(gp *g, inheritTime bool) {// 获取 g_g_ := getg()// 建立 g 和 m 之间的绑定关系_g_.m.curg = gpgp.m = _g_.m// 修改状态信息casgstatus(gp, _Grunnable, _Grunning)gp.waitsince = 0gp.preempt = falsegp.stackguard0 = gp.stack.lo + _StackGuard// 更新 p 的调度次数,为后续61次调度做好准备if !inheritTime {_g_.m.p.ptr().schedtick++}// gogo 将 g0 切换为 g,执行任务gogo(&gp.sched)

主动让渡方法解析

g 执行主动让渡时,会调用 mcall 方法将执行权归还给 g0,并由 g0 调用 gosched_m 方法,位于 runtime/proc.go 文件中:
主动让渡流程

func Gosched() {// ...// 执行mcall让渡mcall(gosched_m)
}// 压栈执行 goschedImpl
func gosched_m(gp *g) {goschedImpl(gp)
}// 实际让渡流程
func goschedImpl(gp *g) {// status := readgstatus(gp)if status&^_Gscan != _Grunning {dumpgstatus(gp)throw("bad g status")}// 改变状态,从running 更改为 runablecasgstatus(gp, _Grunning, _Grunnable)// 解绑 g 和 mdropg()// 加锁 --> 添加到全局队列中 --> 释放锁lock(&sched.lock)globrunqput(gp)unlock(&sched.lock)// 开启新的一轮调度schedule()// 解绑函数
func dropg() {// 获取 g_g_ := getg()// 解绑操作,g 和 m 分别置空setMNoWB(&_g_.m.curg.m, nil)setGNoWB(&_g_.m.curg, nil)
}

gopark 和 goready

g 需要被动调度时,会调用 mcall 方法切换至 g0,并调用 park_m 方法将 g 置为阻塞态,执行流程位于 runtime/proc.go 的 gopark 方法当中:

func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {// ...mcall(park_m)
}func park_m(gp *g) {_g_ := getg()// 修改状态 running 为 waitingcasgstatus(gp, _Grunning, _Gwaiting)// 解绑dropg()// ...// 新的一轮查找schedule()// 当因被动调度陷入阻塞态的 g 需要被唤醒时,会由其他协程执行 goready 方法将 g 重新置为可执行的状态,
// 方法位于 runtime/proc.go .
func goready(gp *g, traceskip int) {systemstack(func() {ready(gp, traceskip, true)})
}// 被动调度如果需要唤醒,则会其他 g 负责将 g 的状态由 waiting 改为 runnable,
// 然后会将其添加到唤醒者的 p 的本地队列中:
func ready(gp *g, traceskip int, next bool) {// ..._g_ := getg()// ...// 修改状态casgstatus(gp, _Gwaiting, _Grunnable)// 重新加入 p 队列中// 如果队列满了,会连带 g 一起将一半的元素转移到全局队列runqput(_g_.m.p.ptr(), gp, next)// ...
}

goexit0 将 g 置于死亡状态

当 g 执行完成时,会先执行 mcall 方法切换至 g0,然后调用 goexit0 方法,内容为 runtime/proc.go:

// Finishes execution of the current goroutine.
func goexit1() {// ...mcall(goexit0)
}// 实际结束方法
func goexit0(gp *g) {// 获取 g_g_ := getg()_p_ := _g_.m.p.ptr()// 更改状态为 deadcasgstatus(gp, _Grunning, _Gdead)// ...// 解绑gp.m = nil// ...// 解绑dropg()// ...// 开启新一轮调度schedule()

抢占调度 retake

抢占调度的执行者不是 g0,而是一个全局的 monitor g,代码位于 runtime/proc.go 的 retake 方法中:

func retake(now int64) uint32 {n := 0// 加锁lock(&allpLock)// 遍历全局的 p 搜索能抢占的目标for i := 0; i < len(allp); i++ {_p_ := allp[i]// p 还没创建if _p_ == nil {// This can happen if procresize has grown// allp but not yet created new Ps.continue}pd := &_p_.sysmontick// ...// 执行系统调用超过 10 ms// p 本地队列有等待执行的 g// 当前没有空闲的 p 和 m.if s == _Psyscall {            // ...if runqempty(_p_) && atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 && pd.syscallwhen+10*1000*1000 > now {continue}unlock(&allpLock)// 抢占调度的步骤// 将当前 p 的状态更新为 idleif atomic.Cas(&_p_.status, s, _Pidle) {n++_p_.syscalltick++// 然后步入 handoffp 方法中,判断是否需要为 p 寻找接管的 m(因为其原本绑定的 m 正在执行系统调用)handoffp(_p_)}incidlelocked(1)// 抢占调度lock(&allpLock)}}unlock(&allpLock)return uint32(n)
}// 判断是否需要 p 接管 m
func handoffp(_p_ *p) {if !runqempty(_p_) || sched.runqsize != 0 {startm(_p_, false)return}if atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) == 0 && atomic.Cas(&sched.nmspinning, 0, 1) {startm(_p_, true)return}lock(&sched.lock)// ...if sched.runqsize != 0 {unlock(&sched.lock)startm(_p_, false)return}// If this is the last running P and nobody is polling network,// need to wakeup another M to poll network.if sched.npidle == uint32(gomaxprocs-1) && atomic.Load64(&sched.lastpoll) != 0 {unlock(&sched.lock)startm(_p_, false)return}// ...

当以下四个条件满足其一时,则需要为 p 获取新的 m:

  1. 当前 p 本地队列还有待执行的 g;
  2. 全局繁忙(没有空闲的 p 和 m,全局 g 队列为空)
  3. 需要处理网络 socket 读写请求

获取 m 时,会先尝试获取已有的空闲的 m,若不存在,则会创建一个新的 m.

func startm(_p_ *p, spinning bool) {mp := acquirem()lock(&sched.lock)// ...// 获取 mnmp := mget()if nmp == nil {// 创建 mid := mReserveID()unlock(&sched.lock)var fn func()// ...// 绑定 pnewm(fn, _p_, id)// ...return}unlock(&sched.lock)// ...
}

reentersyscall 和 exitsyscall

在 m 需要执行系统调用前,会先执行位于 runtime/proc.go 的 reentersyscall 的方法:

func reentersyscall(pc, sp uintptr) {// 此时执行权同样位于 m 的 g0 手中;_g_ := getg()// ...// 保存当前 g 的执行环境;save(pc, sp)_g_.syscallsp = sp_g_.syscallpc = pc// 将 g 和 p 的状态更新为 syscall;casgstatus(_g_, _Grunning, _Gsyscall)// ...// 解除 p 和 当前 m 之间的绑定,因为 m 即将进入系统调用而导致短暂不可用;pp := _g_.m.p.ptr()pp.m = 0// 将 p 添加到 当前 m 的 oldP 容器当中,后续 m 恢复后,会优先寻找旧的 p 重新建立绑定关系._g_.m.oldp.set(pp)_g_.m.p = 0// 将 g 和 p 的状态更新为 syscall;atomic.Store(&pp.status, _Psyscall)// ...

当 m 完成了内核态的系统调用之后,此时会步入位于 runtime/proc.go 的 exitsyscall 函数中,尝试寻找 p 重新开始运作:

func exitsyscall() {// 方法执行之初,此时的执行权是普通 g._g_ := getg()// ...// 倘若此前设置的 oldp 仍然可用,则重新和 oldP 绑定if exitsyscallfast(oldp) {// ...// 将当前 g 重新置为 running 状态,然后开始执行后续的用户函数;casgstatus(_g_, _Gsyscall, _Grunning)// ...return}// ...// old 绑定失败,则调用 mcall 方法切换到 m 的 g0,并执行 exitsyscall0 方法:mcall(exitsyscall0)// ...
}// 
func exitsyscall0(gp *g) {// 将 g 由系统调用状态切换为可运行态,并解绑 g 和 m 的关系casgstatus(gp, _Gsyscall, _Grunnable)dropg()// 加锁 --> 从全局 p 队列获取可用的 plock(&sched.lock)var _p_ *pif schedEnabled(gp) {_p_, _ = pidleget(0)}var locked bool// 如果获取到了,则执行 g:if _p_ == nil {globrunqput(gp)} // 释放锁unlock(&sched.lock)// 如若无 p 可用,则将 g 添加到全局队列,if _p_ != nil {acquirep(_p_)execute(gp, false) // Never returns.}// ...// 当前 m 陷入沉睡. 直到被唤醒后才会继续发起调度.stopm()schedule() // Never returns.
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/828688.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Unity之圆环slider

一、参考文章 Unity_圆环滑动条&#xff08;圆形、弧形滑动条&#xff09;_unity弧形滑动条-CSDN博客 此滑动条拖动超过360后继续往前滑动值会从0开始&#xff0c;正常我们超过360度时不可在滑动。 二、 超过360度不可滑动问题解决 参考HTML文章制作&#xff1a; https://www.c…

SpringCloud系列(15)--Eureka自我保护

前言&#xff1a;在上一章节中我们说明了一些关于Eureka的服务发现功能&#xff0c;也用这个功能进行接口的实现&#xff0c;在本章节则介绍一些关于Eureka的自我保护 1、Eureka保护模式概述 保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。默认情况…

论文辅助笔记:LLM-Mob metric测量

0 导入库 import os import pandas as pd from sklearn.metrics import f1_score import ast import numpy as np1 基本的metric计算方式 1.1 get_acc1_f1 def get_acc1_f1(df):#计算top1 prediction的准确度和f1 scoreacc1 (df[prediction] df[ground_truth]).sum() / le…

开源数据集分享———猫脸码客

猫脸码客作为一个专注于开源数据集分享的公众号&#xff0c;致力于为广大用户提供丰富、优质的数据资源。我们精心筛选和整理各类开源数据集&#xff0c;涵盖机器学习、深度学习、自然语言处理等多个领域&#xff0c;以满足不同用户的需求。 (https://img-blog.csdnimg.cn/d98…

Exploiting CXL-based Memory for Distributed Deep Learning——论文泛读

ICPP 2022 Paper CXL论文阅读笔记整理 问题 深度学习&#xff08;DL&#xff09;正被广泛用于解决不同领域的科学应用中的复杂问题。DL应用程序使用大规模高性能计算&#xff08;HPC&#xff09;系统来训练给定的模型&#xff0c;需要消耗大量数据。这些工作负载具有很大的内…

Git for Windows 下载与安装

当前环境&#xff1a;Windows 8.1 x64 1 打开网站 https://git-scm.com/ &#xff0c;点击 Downloads 。 2 点击 Windows 。 3 选择合适的版本&#xff0c;这里选择了 32-bit Git for Windows Portable。 4 解压下载后的 PortableGit-2.44.0-32-bit.7z.exe &#xff0c;并将 P…

使用 Flask 和 WTForms 构建一个用户注册表单

在这篇技术博客中&#xff0c;我们将使用 Flask 和 WTForms 库来构建一个用户注册表单。我们将创建一个简单的 Flask 应用&#xff0c;并使用 WTForms 定义一个注册表单&#xff0c;包括用户名、密码、确认密码、邮箱、性别、城市和爱好等字段。我们还将为表单添加验证规则&…

好用的在线客服系统PHP源码(开源代码+终身使用+安装教程) 制作第一步

创建一个在线客服系统是一个涉及多个步骤的过程&#xff0c;包括前端界面设计、后端逻辑处理、数据库设计、用户认证、实时通信等多个方面。以下是使用PHP制作在线客服系统的第一步&#xff1a;需求分析和系统设计。演示&#xff1a;ym.fzapp.top 第一步&#xff1a;需求分析 确…

分布式技术在文本摘要生成中的应用

摘要 自然语言处理首先要应对的是如何表示文本以供机器处理&#xff0c;随着网络技术的发展和信息的公开&#xff0c;因特网上可供访问的数字文档成爆炸式的增长&#xff0c;文本摘要生成逐渐成为了自然语言处理领域的重要研究课题。本文主要介绍了分布式技术在文本摘要生成中…

基于springboot+vue+Mysql的广场舞团管理系统

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

猫头虎分享已解决Bug || TypeError: Cannot read property ‘map‘ of undefined**

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

智慧养猪场视频AI智能监控与可视化管理方案

在科技日新月异的今天&#xff0c;智能化、自动化已成为众多行业追求的方向。养猪业作为传统农业的重要组成部分&#xff0c;同样迎来了技术革新的春风。特别是随着人们对食品安全等问题的日益关注&#xff0c;养猪场视频监控监管方案的智能化升级显得尤为重要。 养猪场视频智…

Android11适配

1.分区存储 1.1.背景 Android 11 进一步增强了平台功能&#xff0c;为外部存储设备上的应用和用户数据提供了更好的保护。作为这项工作的一部分&#xff0c;平台引入了进一步的改进&#xff0c;以简化向分区存储的转换。 为了让用户更好地控制自己的文件&#xff0c;保护用户…

(C++) share_ptr 之循环引用

文章目录 &#x1f6a9;前言&#x1f6a9;循环引用&#x1f579;️例子1Code&#x1f62d;shared_ptr &#xff08;错误&#xff09;&#x1f602;weak_ptr &#xff08;正确&#xff09;&#x1f62d;unique_ptr &#xff08;错误&#xff09; &#x1f579;️例子2Code &…

Vu3+QuaggaJs实现web页面识别条形码

一、什么是QuaggaJs QuaggaJS是一个基于JavaScript的开源图像识别库&#xff0c;可用于识别条形码。 QuaggaJs的作用主要体现在以下几个方面&#xff1a; 实时图像处理与识别&#xff1a;QuaggaJs是一款基于JavaScript的开源库&#xff0c;它允许在Web浏览器中实现实时的图像…

LORA详解

参考论文&#xff1a; low rank adaption of llm 背景介绍&#xff1a; 自然语言处理的一个重要范式包括对一般领域数据的大规模预训练和对特定任务或领域的适应处理。在自然语言处理中的许多应用依赖于将一个大规模的预训练语言模型适配到多个下游应用上。这种适配通常是通过…

DiT论文精读Scalable Diffusion Models with Transformers CVPR2023

Scalable Diffusion Models with Transformers CVPR2023 Abstract idea 将UNet架构用Transformer代替。并且分析其可扩展性。 并且实验证明通过增加transformer的宽度和深度&#xff0c;有效降低FID 我们最大的DiT-XL/2模型在classconditional ImageNet 512、512和256、256基…

小程序AI智能名片S2B2C商城系统:四大主流商业模式深度解析与实战案例分享

在私域电商迅速崛起的大背景下&#xff0c;小程序AI智能名片S2B2C商城系统以其独特的商业模式和强大的功能&#xff0c;正成为品牌商们争相探索的新领域。在这一系统中&#xff0c;拼团模式、会员电商、社区团购和KOC营销等四种主流模式&#xff0c;为品牌商提供了多样化的营销…

【文章转载】Lance Martin的关于RAG的笔记

转载自微博黄建同学 从头开始学习 RAG&#xff0c;看Lance Martin的这篇笔记就行了&#xff0c;包含了十几篇论文和开源实现&#xff01; —— 这是一组简短的&#xff08;5-10 分钟视频&#xff09;和笔记&#xff0c;解释了我最喜欢的十几篇 RAG 论文。我自己尝试实现每个想…

C# GetField 方法应用实例

目录 关于 C# Type 类 GetField 方法应用 应用举例 心理CT设计题 类设计 DPCT类实现代码 小结 关于 C# Type 类 Type表示类型声明&#xff1a;类类型、接口类型、数组类型、值类型、枚举类型、类型参数、泛型类型定义&#xff0c;以及开放或封闭构造的泛型类型。调用 t…