当前位置:首页 > 后端开发 > 正文内容

Golang的GMP调度模型与源码解析

邻居的猫1个月前 (12-09)后端开发418

0、导言

咱们知道,这今世操作体系中,多线程和多进程模型被广泛的运用以进步体系的并发功率。跟着互联网不断的开展,面临现在的高并发场景,为每个使命都创立一个线程是不现实的,运用线程则需求体系不断的在用户态和内核态之间不断的切换,引起不必要的损耗,所以引入了协程。协程存在于用户空间,是一种轻量级的并发履行单元,其创立和上下文的开支更小,怎样办理数量很多的协程是一个重要的论题。此篇笔记用于共享笔者学习Go言语协程调度的GMP模型的了解,以及源码的完结。当时运用的Go言语版别为1.22.4。

本篇笔记参阅了以下文章:

[Golang三关-典藏版] Golang 调度器 GMP 原理与调度全剖析 | Go 技术论坛

Golang GMP 原理

Golang-gopark函数和goready函数原理剖析

1、GMP模型拆解

Goroutine调度器的作业是将预备作业的goroutine分配到作业线程上,涉及到的首要概念如下:

1.1、G

G代表的是Goroutine,是Go言语对协程概念的笼统,其有以下的特色:

  • 是一个轻量级的线程
  • 具有自己的栈、状况、以及履行的使命函数
  • 每一个G会被分配到一个可用的P,而且在M上作业

其结构界说坐落runtime/runtime2.go中:

type g struct {
    // ...
    m         *m      
    // ...
    sched     gobuf
    // ...
}

type gobuf struct {
    sp   uintptr
    pc   uintptr
    ret  uintptr
    bp   uintptr // for framepointer-enabled architectures
}

在这儿,咱们中心重视其内嵌了一个m和一个gobuf类型的sched。gobuf首要用于Gorutine的上下文切换,其保存了G履行过程中的CPU寄存器的状况,使得G在暂停、调度和康复作业时能够正确地康复上下文。

G首要有以下几种状况:

const (
	_Gidle = iota // 0
	_Grunnable // 1
	_Grunning // 2
	_Gsyscall // 3
	_Gwaiting // 4
    //...
	_Gdead // 6
    //...
	_Gcopystack // 8
    _Gpreempted // 9
	//...
)
  • Gidle:表明这个G刚刚被分配,没有初始化。

  • Grunnable:表明这个G在作业行列中,它当时不再履行用户代码,栈未被占用。

  • Grunning:表明这个G或许在履行用户代码,栈被这个G占用,它不在作业行列中,而且它被分配给了一个M和一个P(g.m和g.m.p是有用的)。

  • Gsyscall:表明这个G正在履行体系调用,它不在履行用户代码,栈被这个G占用。它不在作业行列中,而且它被分配给了一个M。

  • Gwaiting:表明这G被堵塞在作业时,它没有履行用户代码,也不在作业行列中,可是它应该被记录在某个当地,以便在必要时将其唤醒。(ready())gc、channel 通讯或许锁操作时经常会进入这种状况。

  • Gdead:表明这个G当时未运用,它或许是刚被初始化,也或许是现已被毁掉。

  • Gcopystack:表明这个G的栈正在被移动。

  • Gpreempted:表明这个G因抢占而被挂起,且该G自行中止,等候进一步的康复。它类似于Gwaiting,可是Gpreempted还没有一个担任将其状况康复的办理者,只要某个suspendG操作将该G的状况从Gpreempted转化为Gwaiting,这样调度器才会接收这个G。

在阅览有关调度逻辑的源码的时分,咱们能够经过查找casgstatus办法去定位到使得G状况改动的函数,例如:casgstatus(gp, _Grunning, _Gsyscall)表明将该G的状况从Grunning变换到Gsyscall,就能够找到对应的函数学习了。

1.2、M

M是Machine,也是Worker Thread,代表的是操作体系的线程。Go作业时在需求时创立或许毁掉M,将G组织到M上履行,充分利用多核CPU的才能。其具有以下的特色:

  • M是Go与操作体系之间的桥梁,它担任履行分配给它的G。
  • M的数量会依据体系资源进行调整。
  • M或许会被特定的G经过LockOSThread确定,这种G和M的绑定确保了特定Goroutine能够持续运用同一个线程。

结构界说如下:

type m struct{
	g0      *g     // goroutine with scheduling stack
	curg          *g       // current running goroutine
	tls           [tlsSlots]uintptr // thread-local storage (for x86 extern register)
	p             puintptr // attached p for executing go code (nil if not executing go code)
	oldp          puintptr // the p that was attached before executing a syscall
	//...
}

每一个M结构体都会有一个名为g0的G,它是一个特别的Goroutine,它并不杂乱履行用户的代码,而是担任调度G。g0会分配G绑定到M中履行。tls表明的是“Local Thread Storage”,其存储了与当时线程相关的特定信息,而tls数组的第一个槽位一般用于存储g0的栈指针。

M存在一个状况,名为“自旋态”,处在自旋态的M会不断的往大局行列中寻觅可作业的G去履行,而且免除自旋态。

1.3、P

P是Processor,代表逻辑处理器,是Goroutine调度的虚拟概念。每个P担任分配履行Goroutine的资源,其具有以下的特色:

  • P是G的履行上下文,它具有一个本地行列存储着G,以及对应的使命调度机制,担任在M上履行一个详细的G。
  • P的数量由环境变量GOMAXPROCS决议,假设其数量大于CPU的物理线程数量时就没有更多的含义了。
  • P是去履行Go代码所必备的资源,M有必要绑定了一个P才能去履行Go代码。可是M能够在没有绑定P的状况下履行体系调用或许被堵塞。
type p struct {
	status      uint32
	runqhead uint32
	runqtail uint32
	runq     [256]guintptr
	m           muintptr
	runnext guintptr
	//...
}
  • runq存储了这个P具有的goroutine行列,最大长度为256
  • runqhead和runqtail别离指向行列的头部和尾部
  • runnext存储了下一个可履行的goroutine

P也含有几个状况,如下:

const (
	_Pidle = iota
	_Prunning
	_Psyscall
	_Pgcstop
	_Pdead
)
  • Pidle:表明P没有被作业用户代码或许调度器,一般这个P在闲暇P列表中,供调度器运用,但它也或许在其他状况之间转化。P由闲暇行列idle list或许其他转化其状况的目标具有,它的runq是空的。
  • Prunning:表明P被M具有,而且正在作业用户代码或许调度器。只要具有此P的M被答应更改P的状况,M能够将P转化为Pidle(当没有作业的时分)、Psyscall(当进入一个体系调用时)、Pgcstop(组织废物收回时)。M还能够将P的一切权交接给另一个M(例如调度一个locked的G)
  • Psyscall:表明P没有在作业用户代码,与在体系调用中的M相关但不被其具有。处于Psyscall状况的P或许会被其他M抢走。将P转化给另一个M是轻量级的,而且P会坚持和原始的M的关联性。
  • Pgcstop:表明P被暂停以进行STW(Stop The World)(履行废物收回)。
  • Pdead:表明P不再被运用(GOMAXPROCS削减)。死去的P将会被掠夺资源,可是任然会保存少数的资源例如Trace Buffer,用于后续的盯梢剖析需求。

1.4、Schedt

schedt是大局goroutine行列的封装

type schedt struct {
    // ...
    lock mutex
    // ...
    runq     gQueue
    runqsize int32![](https://img2024.cnblogs.com/blog/3542244/202411/3542244-20241117153220788-1594654379.png)

    // ...
}
  • lock:是操作大局行列的锁
  • runq:存储G的行列
  • runqsize:大局G行列的容量

2、调度模型的作业流程

咱们能够用下图来全体的表明该调度模型的流程:

在接下来的部分,咱们将首要讨论GMP调度模型是怎样完结一轮调度的,便是怎样完结g0到g再到g0的切换的,期间大致发生了什么。

2.1、G的状况转化

咱们刚刚提及到,每一个M都有一个名为g0的Goroutine,去担任调度一般的g绑定到M上履行。g0和一般的g之间存在一个转化,当履行一般的g上的代码的时分,就会将履行权交给g,当g履行完代码或许由于原因需求被挂起、退出履行等,就会从头将履行权交给g0。

g0和P是一个协作的联系,P的行列决议了哪些goroutine能够在绑定P时被调用,而g0是履行调度逻辑的要害的goroutine,担任在必要时开释P的资源。

当g0需求将履行权交给g时,会调用一个名为gogo的办法,传入g的栈指针,去履行用户的代码。

func gogo(buf *gobuf)

当需求从头将履行权转交给g0时,都会履行一个名为mcall的办法。

func mcall(fn func(*g))

mcall在go需求进行协程互换时被调用,它传入一个回调函数fn,里边带着了当时正在作业的g的指针,它首要做了以下三点的作业:

  • 保存当时g的信息,行将PC/SP的信息存储到g->sched中,确保后续能够康复g的履行现场。
  • 将当时M的仓库从g切换到g0
  • 在g0的栈上履行新的函数fn,一般在fn中会进一步组织g的去向,而且调用schedule函数,让当时M去寻觅另一个能够履行的G。

2.2、调度类型

咱们现在知道了,g和g0是经过什么函数进行状况切换的。接下来咱们就要来讨论,它们是什么状况下要进行切换,即调度战略有什么。

GMP调度模型一共有4种调度战略,别离为:自动调度被迫调度正常调度抢占调度

  • 自动调度:提供给用户的办法,当用户调用了runtime.Gosched()办法时,此刻当时的g会让出履行权,将g组织进使命行列等候下一次被调度。
  • 被迫调度:当因不满足某种履行条件,一般为channel读写条件不满足时,会履行gopark()函数,此刻的g将会被置为等候状况。
  • 正常调度:g正常的履行结束,转接履行权。
  • 抢占调度:存在一个大局监控者moniter,它会每隔一段时刻周期去查看是否有G作业太长时刻,若发现了,将会告诉P去进行和M的解绑,让出P。这儿需求大局监控者的存在是由于当G进入到体系调用的时分,这个线程M会堕入相持,无法自动去查看,需求外援辅佐。

2.3、微观调度流程

接下来咱们来重视全体一轮的调度流程,关于g0和g的一轮调度,能够用下图来表明。

schedule作为每一轮调度的开端,它会寻觅到能够履行的G,然后调用execute将该g绑定到一个线程M上,然后履行gogo办法去真实的作业一个goroutine。当需求转化时,goroutine会在底层履行mcall办法,保存栈信息,然后履行回调函数fn,即绿框内的办法之一,将履行权从头交给g0。

2.3.1、schedule()

schedule()办法定坐落runtime/proc中,疏忽非主流程部分,源码内容如下:

//找到一个是安排妥当态的G去作业
func schedule() {
	mp := getg().m

	//...

top:
	pp := mp.p.ptr()
	pp.preempt = false

	//假设该M在自旋,可是行列含有G,那么抛出反常。
	if mp.spinning && (pp.runnext != 0 || pp.runqhead != pp.runqtail) {
		throw("schedule: spinning with local work")
	}

	gp, inheritTime, tryWakeP := findRunnable() //堵塞的寻觅G

	
    //...

	//当时M即将作业一个G,免除自旋状况
	if mp.spinning {
		resetspinning()
	}

	//...

	execute(gp, inheritTime)
}

该办法首要是寻觅一个能够作业的G,交给该线程去作业。咱们在一开端说到,线程会存在一种名为“自旋态”的状况,它会不断的自旋去寻觅能够履行的G来履行,成功找到了就免除了自旋态。

这儿存在一个点咱们值得去留意,处在自旋态的线程它不是在空占用核算资源吗?那么不就是降低了体系的功能吗?

其实这是一个中和的战略,假设每次当呈现了一个新的Goroutine需求去履行的时分,咱们才创立一个线程M去履行它,然后履行完了又删除去不去复用,那么就会带来很多的创立毁掉的资源耗费。咱们期望当有一个新的Goroutine来的时分,能当即有一个M去履行它,就能够将闲暇暂时无使命处理的M去自己寻觅Goroutine,削减了创立毁掉的资源耗费。可是咱们也不能有太多的处于自旋态的线程,不然就造就另一个过多耗费的当地了。

咱们先跟进一下resetspinning(),看看其履行的战略是什么。

1、resetspinning()

func resetspinning() {
	gp := getg()
	//...
	gp.m.spinning = false
	nmspinning := sched.nmspinning.Add(-1)
	//...
	wakep()
}



//测验增加一个P去履行G。该办法被调用当一个G状况为runnable时。
func wakep() {
    //假设自旋的M数量不为0则回来
	if sched.nmspinning.Load() != 0 || !sched.nmspinning.CompareAndSwap(0, 1) {
		return
	}

	// 禁用抢占,直到 pp 的一切权搬运到 startm 中的下一个 M,不然在这儿的抢占将导致 pp 被卡在等候进入 _Pgcstop 状况。
	mp := acquirem()

	var pp *p
	lock(&sched.lock)
    //测验从闲暇P行列获取一个P
	pp, _ = pidlegetSpinning(0)
	if pp == nil {
		if sched.nmspinning.Add(-1) < 0 {
			throw("wakep: negative nmspinning")
		}
		unlock(&sched.lock)
		releasem(mp)
		return
	}
	
	unlock(&sched.lock)

	startm(pp, true, false)

	releasem(mp)
}

resetspinning中,咱们先将当时M免除了自旋态,然后测验去唤醒一个P,即进入到wakep()办法中。

if sched.nmspinning.Load() != 0 || !sched.nmspinning.CompareAndSwap(0, 1) {
		return
	}

在wakep办法内,咱们先查看了当时处在自旋的M的数量,假设>0,则不再去唤醒一个新的P,这是为了防止同一时刻内过多的自旋的M空作业耗费CPU资源。

pp, _ = pidlegetSpinning(0)
	if pp == nil {
		if sched.nmspinning.Add(-1) < 0 {
			throw("wakep: negative nmspinning")
		}
		unlock(&sched.lock)
		releasem(mp)
		return
	}

接着会测验从闲暇P行列中获取一个P,假设没有闲暇的P,那么此刻会削减自旋线程的数量(这儿仅仅削减了数量,可是详细这个处在自旋的线程接下往来不断做什么了我也没有理解)而且回来。

startm(pp, true, false)

假设获取了一个闲暇的P,会为这一个P分配一个线程M。

2、findRunnable()

findRunnable是一轮调度流程中最中心的办法,它用于找到一个可履行的G。

func findRunnable() (gp *g, inheritTime, tryWakeP bool) {
	mp := getg().m
top:
    pp := mp.p.ptr()
	//...
 	
    //每61次调度周期就查看一次大局G行列,防止在特定状况只依赖于本地行列。
	if pp.schedtick%61 == 0 && sched.runqsize > 0 {
		lock(&sched.lock)
		gp := globrunqget(pp, 1)
		unlock(&sched.lock)
		if gp != nil {
			return gp, false, false
		}
	}
    //...
    // local runq
	if gp, inheritTime := runqget(pp); gp != nil {
		return gp, inheritTime, false
	}

	// global runq
	if sched.runqsize != 0 {
		lock(&sched.lock)
		gp := globrunqget(pp, 0)
		unlock(&sched.lock)
		if gp != nil {
			return gp, false, false
		}
	}
    
    //在正式的去盗取G之前,用非堵塞的办法查看是否有安排妥当的网络协程,这是对netpoll的一个优化。
	if netpollinited() && netpollAnyWaiters() && sched.lastpoll.Load() != 0 {
		if list, delta := netpoll(0); !list.empty() { // non-blocking
			gp := list.pop()
			injectglist(&list)
			netpollAdjustWaiters(delta)
			trace := traceAcquire()
			casgstatus(gp, _Gwaiting, _Grunnable)
			if trace.ok() {
				trace.GoUnpark(gp, 0)
				traceRelease(trace)
			}
			return gp, false, false
		}
	}
    
    //假设当时的M出于自旋状况,或许说处于自旋状况的M的数量小于活泼的P数量的一半时,则进行G盗取。(防止当体系的并行度较低时,自旋的M过多占用CPU资源)
	if mp.spinning || 2*sched.nmspinning.Load() < gomaxprocs-sched.npidle.Load() {
		if !mp.spinning {
			mp.becomeSpinning()
		}

		gp, inheritTime, tnow, w, newWork := stealWork(now)
		if gp != nil {
			// Successfully stole.
			return gp, inheritTime, false
		}
		if newWork {
			// There may be new timer or GC work; restart to
			// discover.
			goto top
		}

		now = tnow
		if w != 0 && (pollUntil == 0 || w < pollUntil) {
			// Earlier timer to wait for.
			pollUntil = w
		}
	}
    
    //...

其首要的履行过程如下:

(一)第六十一次调度
if pp.schedtick%61 == 0 && sched.runqsize > 0 {
		lock(&sched.lock)
		gp := globrunqget(pp, 1)
		unlock(&sched.lock)
		if gp != nil {
			return gp, false, false
		}
	}

首要查看P的调度次数,假设这次是P的第61此次调度,而且大局的G行列长度>0,就会从大局行列获取一个G。这是为了防止在特定状况下,只作业本地行列的G,忽视了大局行列。

其内部调用的globrunqget办法主流程如下:

//测验从G的大局行列获取一批G
func globrunqget(pp *p, max int32) *g {
	assertLockHeld(&sched.lock)
	//查看大局行列是否为空
	if sched.runqsize == 0 {
		return nil
	}

    //核算需求获取的G的数量
	n := sched.runqsize/gomaxprocs + 1
	if n > sched.runqsize {
		n = sched.runqsize
	}
	if max > 0 && n > max {
		n = max
	}
    //确保从行列中获取的G数量不超越当时本地行列的G数量的一半,防止大局行列一切的G都搬运到本地行列中导致负载不均衡
	if n > int32(len(pp.runq))/2 {
		n = int32(len(pp.runq)) / 2
	}
	sched.runqsize -= n

	gp := sched.runq.pop()
	n--
	for ; n > 0; n-- {
		gp1 := sched.runq.pop()
		runqput(pp, gp1, false)
	}
	return gp
}
//核算需求获取的G的数量
	n := sched.runqsize/gomaxprocs + 1
	if n > sched.runqsize {
		n = sched.runqsize
	}
	if max > 0 && n > max {
		n = max
	}
	if n > int32(len(pp.runq))/2 {
		n = int32(len(pp.runq)) / 2
	}

n为要从大局G行列获取的G的数量,能够看到它会至少获取一个G,至多获取runqsize/gomaxprocs+1个G,它确保了一个P不过多的获取G然后影响负载均衡。而且不答应n一次获取大局G行列一半以上的G,确保负载均衡。

gp := sched.runq.pop()
	n--
	for ; n > 0; n-- {
		gp1 := sched.runq.pop()
		runqput(pp, gp1, false)
	}

决议好获取多少个G后,第一个G会直接经过指针回来,剩下的则是将其增加到P的本地行列中。

在当时(一)的调用中,函数设置了max值为1,因而只会从大局行列获取1个G回来。


虽然在(一)中不会履行runqput,可是咱们仍是来看看是怎样将G增加到P的本地行列的。

// runqput测验将G放到本地行列中
//假设next是False,runqput会将G增加到本地行列的尾部
//假设是True,runqput会将G增加到下一个将被调度的G的槽位
//假设作业行列满了,那么将会把g放回大局行列
func runqput(pp *p, gp *g, next bool) {
    //
	if randomizeScheduler && next && randn(2) == 0 {
		next = false
	}

	if next {
	retryNext:
		oldnext := pp.runnext
		if !pp.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) {
			goto retryNext
		}
		if oldnext == 0 {
			return
		}
		// Kick the old runnext out to the regular run queue.
		gp = oldnext.ptr()
	}

retry:
	h := atomic.LoadAcq(&pp.runqhead) //加载行列头的方位
	t := pp.runqtail
	if t-h < uint32(len(pp.runq)) { //查看本地行列是否已满
		pp.runq[t%uint32(len(pp.runq))].set(gp) //未满将gp刺进runqtail的指定方位
		atomic.StoreRel(&pp.runqtail, t+1) //更新runtail,表明刺进的G可供消费
		return
	}
	if runqputslow(pp, gp, h, t) { //假设本地行列已满,则测验放回大局行列
		return
	}
	// the queue is not full, now the put above must succeed
	goto retry
}
if randomizeScheduler && next && randn(2) == 0 {
		next = false
	}

在第一步中,咱们看到即便next被设置为true,即要求了该G应该被放置在本地P行列的runnext槽位中,也会有概率地将next置为false

if next {
	retryNext:
		oldnext := pp.runnext
		if !pp.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) {
			goto retryNext
		}
		if oldnext == 0 {
			return
		}
		// Kick the old runnext out to the regular run queue.
		gp = oldnext.ptr()
	}

假设next仍为true,此刻先获取本来P调度器中,runnext槽位的G(oldnext),然后会不断地测验将新的G替换掉旧的G直到成功停止。当成功之后,鄙人面的操作流程中会把旧的G放入到P的本地行列中。

retry:
	h := atomic.LoadAcq(&pp.runqhead) //加载行列头的方位
	t := pp.runqtail
	if t-h < uint32(len(pp.runq)) { //查看本地行列是否已满
		pp.runq[t%uint32(len(pp.runq))].set(gp) //未满将gp刺进runqtail的指定方位
		atomic.StoreRel(&pp.runqtail, t+1) //更新runtail,表明刺进的G可供消费
		return
	}
	if runqputslow(pp, gp, h, t) { //假设本地行列已满,则测验放回大局行列
		return
	}
	// the queue is not full, now the put above must succeed
	goto retry
}

在将G参加进P的本地行列的流程中,需求获取行列头部和尾部的坐标,用来判别本地行列是否已满,未满则将G刺进进本地行列的尾部中。不然履行runqputslow办法,测验放回大局行列。


接下来持续跟进runqputslow办法的履行流程。

//将G和一批作业(本地行列的G)放入到大局行列
func runqputslow(pp *p, gp *g, h, t uint32) bool {
	var batch [len(pp.runq)/2 + 1]*g //本地行列一半的G

	// First, grab a batch from local queue.
	n := t - h
	n = n / 2
	if n != uint32(len(pp.runq)/2) {
		throw("runqputslow: queue is not full")
	}
	for i := uint32(0); i < n; i++ {
		batch[i] = pp.runq[(h+i)%uint32(len(pp.runq))].ptr()
	}
	if !atomic.CasRel(&pp.runqhead, h, h+n) { // cas-release, commits consume
		return false
	}
	batch[n] = gp

	if randomizeScheduler { //打乱次序
		for i := uint32(1); i <= n; i++ {
			j := cheaprandn(i + 1)
			batch[i], batch[j] = batch[j], batch[i]
		}
	}

	// Link the goroutines.
	for i := uint32(0); i < n; i++ {
		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
}

其履行流程如下:

var batch [len(pp.runq)/2 + 1]*g //本地行列一半的G

首要创立一个batch数组,是容量为P的本地行列当时含有的G的数量的一半,用于存储将搬运的G。

n := t - h
	n = n / 2
	if n != uint32(len(pp.runq)/2) {
		throw("runqputslow: queue is not full")
	}
	for i := uint32(0); i < n; i++ {
		batch[i] = pp.runq[(h+i)%uint32(len(pp.runq))].ptr()
	}

接着,开端将本地行列一半的G的指针,存储在batch中。

if randomizeScheduler { //打乱次序
		for i := uint32(1); i <= n; i++ {
			j := cheaprandn(i + 1)
			batch[i], batch[j] = batch[j], batch[i]
		}
	}

然后会打乱batch中的次序,确保随机性。

// Link the goroutines.
	for i := uint32(0); i < n; i++ {
		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

最终一部是将batch中的各个G用指针衔接起来,转化为链表的方法,而且链接在大局行列中。

runqput衔接的流程较长,用下图来归纳:

(二)本地行列获取
// local runq
	if gp, inheritTime := runqget(pp); gp != nil {
		return gp, inheritTime, false
	}

假设不是第61次调用,findrunnable会测验从本地行列中获取一个G用于调度。咱们来看runqget办法的履行。

// 从本地可作业行列中获取 g。
func runqget(pp *p) (gp *g, inheritTime bool) {
	// 假设有 runnext,则它是下一个要作业的 G。
	next := pp.runnext
    // 假设 runnext 非零且 CAS 操作失利,它只能被另一个 P 盗取,由于其他 P 能够竞赛将 runnext 设置为零,但只要当时 P 能够将其设置为非零。
	// 因而,假设 CAS 失利,则无需重试该操作。
	if next != 0 && pp.runnext.cas(next, 0) {
		return next.ptr(), true
	}

	for {
		h := atomic.LoadAcq(&pp.runqhead) // load-acquire, synchronize with other consumers
		t := pp.runqtail
		if t == h {
			return nil, false
		}
		gp := pp.runq[h%uint32(len(pp.runq))].ptr()
		if atomic.CasRel(&pp.runqhead, h, h+1) { // cas-release, commits consume
			return gp, false
		}
	}
}

假设能够获取到P的runnext,则回来这一个G,不然就获取本地行列的头部的G。

(三)大局行列获取
// global runq
	if sched.runqsize != 0 {
		lock(&sched.lock)
		gp := globrunqget(pp, 0)
		unlock(&sched.lock)
		if gp != nil {
			return gp, false, false
		}
	}

假设无法从本地行列获取到G,则说明晰P的本地行列为空,此刻会测验从大局行列获取G。调用了globrunqget办法从大局行列获取G,留意此刻由于设置了max为0表明不收效,该办法或许会从大局行列中获取多个G放到P的本地行列内。关于该办法的详细代码现已在(一)中解说。

(四)网络事情获取
    //在正式的去盗取G之前,用非堵塞的办法查看是否有安排妥当的网络协程,这是对netpoll的一个优化。
	if netpollinited() && netpollAnyWaiters() && sched.lastpoll.Load() != 0 {
		if list, delta := netpoll(0); !list.empty() { // non-blocking
			gp := list.pop()
			injectglist(&list)
			netpollAdjustWaiters(delta)
			trace := traceAcquire()
			casgstatus(gp, _Gwaiting, _Grunnable)
			if trace.ok() {
				trace.GoUnpark(gp, 0)
				traceRelease(trace)
			}
			return gp, false, false
		}
	}

假设本地行列和大局行列都没有G能够获

扫描二维码推送至手机访问。

版权声明:本文由51Blog发布,如需转载请注明出处。

本文链接:https://www.51blog.vip/?id=177

分享给朋友:

“Golang的GMP调度模型与源码解析” 的相关文章

Kraft形式下Kafka脚本的运用

Kraft形式下Kafka脚本的运用

Kafka集群 版别:V3.5.1 称号 Node1 Node2 Node3 IP 172.29.145.157 172.29.145.182 172.29.145.183 (1)检查Kraft集群中的状况以及Leader节点,投票节点 运用--status能够检查集群推举次数/水位线以及投票节点等...

rust腐蚀多少钱,了解其经济影响

目前《Rust(腐蚀)》在Steam国区的售价为136元人民币。如果你不急于购买,可以留意Steam上的促销活动,有时会有折扣,最低曾达到25元人民币Rust腐蚀的代价:了解其经济影响在工业领域,Rust(铁锈)是一种常见的腐蚀现象,它不仅影响设备的性能和寿命,还会带来显著的经济损失。本文将探讨Ru...

python计算器简单代码, 环境准备

当然可以。下面是一个简单的Python计算器代码示例,它能够执行基本的加、减、乘、除运算:```pythondef simple_calculator: operation = input: qwe2 num1 = floatqwe2 num2 = floatqwe2 if...

php显示图片, 图片路径处理

php显示图片, 图片路径处理

在PHP中显示图片可以通过多种方式实现,下面我将介绍几种常见的方法:```html``` 2. 使用PHP读取图片并输出如果你想在PHP脚本中动态生成图片或者从数据库中读取图片并显示,你可以使用PHP的文件处理函数来读取图片文件的内容,然后输出它。这里有一个简单的例子:```php// 检查文件是否...

c语言函数返回数组,C语言函数返回数组的实现与注意事项

c语言函数返回数组,C语言函数返回数组的实现与注意事项

在C语言中,函数不能直接返回一个数组。但是,你可以通过以下几种方式间接地实现:1. 返回指向数组的指针:你可以让函数返回一个指向数组的指针。但这种方式需要你确保返回的指针所指向的数组在函数返回后仍然有效。一种常见的做法是使用静态数组,因为静态数组在函数返回后仍然存在,但它的缺点是每次调用函数时,数组...

计算机二级c语言答案,计算机二级C语言考试答案解析

计算机二级c语言答案,计算机二级C语言考试答案解析

你可以通过以下资源获取计算机二级C语言的真题及答案:1. 历年计算机二级C语言真题及答案: 这里提供了历年计算机二级C语言的真题及答案,可以在线评测。2. C语言二级题库带答案 解析: 该文档包含程序设计题的题目、程序(含空)以及答案,适合进行练习。3. 2023年全国计...