Golang 1.21.4 GMP调度器底层实现个人走读

1 原理

1.1 GMP调度模型的设计思想

1.1.1 传统多线程的问题

在现代的操作系统中,为了提高并发处理任务的能力,一个CPU核上通常会运行多个线程,多个线程的创建、切换使用、销毁开销通常较大:

(1)一个线程的大小通常达到4M,因为需要分配内存来存放用户栈和内核栈的数据

(2)一个线程执行系统调用(发生IO时间如网络请求或者读写请求)不占用CPU时,需要及时让出CPU,交给其他线程执行,这是会发生线程之间的切换

(3)线程在CPU上进行切换时,需要保持当前线程的上下文,将待执行的线程的上下文恢复到寄存器中,还需要向操作系统内核申请资源

在高并发的情况下,大量线程的创建、使用、切换、销毁会占用大量的内存,并浪费较多的CPU时间在非工作任务的执行上,导致程序并发处理事务的能力降低

1.2 GO早期的GM模型 

为了解决传统内核级的线程的创建、切换、销毁开销较大的问题,Go将线程分为了内核级线程M,轻量级的用户态的协程Goroutine。(简单点来说就是没什么问题是加一层逻辑处理不了,如果有就再加一层,既然内核的线程开销大,那我就自己在内核级的线程之上创建一个更轻量的线程)。到这里,Go的调度器的三个核心概念就出现了两个:

M:Machine,代表了内核级线程OS Thread,CPU调度的基本单元

G:Goroutine,被Go优化过的协程,是一个用户态、轻量级的协程,一个G代表了对一段需要被执行Go程序的封装。每个Goroutine都有自己独立地栈存放自己程序的运行状态,分配的栈大小为2KB,可以按需扩容。

在早期,Go将传统线程拆分成为了M和G之后,为了充分利用轻量级的G的低内存占用、低切换开销 的优点,会在当前一个M上绑定多个G,某个正在运行中G执行完成后,Go调度器会将该G切换走,将其他可以运行的G放入M上执行,这时一个Go程序中只有一个M线程

 

 这个方案的优点是用户态的G可以快速切换,不会陷入内核态,缺点是无法充分利用CPU,并且如果G阻塞,会导致跟G绑定的M阻塞,其他G也用不了M去执行自己的程序了。

为了解决这些问题,Go后面又上线了多线程调度器,如下:

每个Go程序都有多个M线程对应的多个G,但是每次取、还G的时候都需要进行加锁操作,这样会导致锁竞争导致性能下降。

1.1.3 当前的GMP模型

为了解决上面的问题,Go在上面M,G的基础上,引入了P处理器。

P:Process的缩写,代表一个虚拟的处理器,它维护了一个局部可运行的G队列,可以通过CAS的方式无锁访问,除了自己,别人也有可能从自己这里偷G(简单的提一下,这里说的无锁主要理解为轻量的加锁,个人觉得这个atomic原子化操作更像一把乐观锁,没有互斥锁锁的粒度范围大,具体取G流程下面会介绍。)工作线程M优先使用自己的runnext里面存放的G,没有才去本地环形队列找G,只有前面两个都找不到才会去全局队列中找G,这就大大减少了锁冲突,提高了大量G的并发性。每个G想要在M上运行,必须被分配一个P(sysmon除外,这个大哥不需要p就能直接在M上运行,后续介绍)

当前Go采用的GMP调度模型如下图所示。可运行的G是通过处理器P和线程M绑定起来的,M的执行是由操作系统调度器将M分配到CPU上实现的,Go运行时调度器负责调度G到M上执行,主要在用户态运行,跟操作系统调度器在内核态运行相对应。

(这里的G为什么是放在P上而不是M上?这是因为当一个线程M阻塞(有可能是因为执行系统调用或者IO请求)的时候,可以将和它绑定的P和G转移到其他线程M上去执行,如果直接把可运行的G组成的本地队列绑定到M,这个时候万一M阻塞,它拥有的G就不能给其他M执行,后续介绍) 

1.1.4 Go调度器的核心思想

1.尽可能复用线程M:主要是为了避免现成的创建和销毁

2.利用多核并行能力:限制同时运行(不包含阻塞)的M线程数为N,N等于CPU的核心数目,这里可以通过设置P处理器的个数为GOMAXPROCS来保证(也就是平常代码写的runtime.GOMAXPROCS(n),注意,这个还是少用,因为会STW也就是stopTheWorld停止一切工作)。不设置默认为CPU核数,因为M和P是绑定的,没有找到P的M会放入空闲M列表,没有找到M的P也会放入空闲列表

3.Work Stealing任务窃取机制:M优先执行其所绑定的P的runnext的G,没有就去P的本地环形队列找,再没有就去全局链表找,再没有就回去别的P里面去偷G来运行(主要是为了所谓的负载均衡,不让每个自旋的M闲着)

4.Hand Off移交机制:M阻塞,会将M绑定的P移交给别的M绑定,然后接着执行P存储的G

5.基于协作的抢占机制:每个真正运行的G,如果不被打断,将会一直被运行下去,这样太不公平(毕竟有些总喜欢占着茅坑不拉屎),为了公平,也就是防止新建的G一直P的本地队列不被调度导致饥饿问题,Go程序会保证每个G运行10ms就要让出M,交给其他G执行(sysmon负责监控,后续介绍)

6.基于信号的真抢占机制:尽管基于协作的抢占机制能够缓解长时间GC导致整个程序无法工作和大多数Goroutine饥饿问题,但是还是有部分情况下,Go调度器有无法被抢占的情况,例如,for循环或者垃圾回收长时间占用线程,为了解决这些问题,Go1.14引入了基于信号的抢占式调度机制,能够解决GC垃圾回收和栈扫描时存在的问题。(此机制后续不介绍,别问,问就是我没看懂,太菜了)

1.1.5 Go的调度流程(注意Go版本不一样情况可能也不一样,请注意版本哦,亲)

Go的调度流程的本质就是一个生产-消费流程

生产端

情况一:runnext为空,local run queue为空,global run queue为空,此时M1正在运行G1,然后G1创建了一个G2

1.21.4这个版本新建的G不是一定先放入runnext,有的版本是,看你用的是啥版本,到时候看看源码就知道,就跟买彩票一样,有概率会被放入runnext,如果没有中奖,也就是没有放入runnext ,那就会直接进入local run queue

网上很多博客没有提这个runnext的哦,亲,不要有疑问,源码就是有,不信接着往下看源码

情况二:runnext满,local run queue满,global run queue可空可不空随便,反正是链表,此时M1正在运行G1,然后G1又创建了一个Gn

此时会先将本地队列的前一半先取出来放到数组batch中,然后把新建的Gn放到batch最后,然后打乱batch中的存放顺序,再按照batch的顺序存放到全局链表中

再次声明,每个版本可能情况不同,具体看源码哦,亲,不要纠结,网上博客一人说一个,你自己去看源码,那才是最正确的答案

消费端

情况一:runnext不为空,local run queue不为空,global run queue随意(现在跟他没啥关系)

此时M1刚执行完G1,然后切换G0,开始寻找新的G来调度,优先从runnext,然后是local run queue,最后是global queue,先不要过分关注细节,后续源码展开,先了解大概

 情况二:runnext为空,local run queue为空,global run queue为空,此时M1刚执行完G1,切换G0开始寻找下一个可运行的G,因为runnext为空,local run queue为空,global run queue也为空,G0只能走下下策,随机找个P偷它的G用。具体的偷细节看源码。下面简单描述一下

假设被偷的是P1,小偷是P0的G0,一共有四次容错,优先偷P1本地队列的G,当然,G0身为小偷,必然要给人家留点G,所以会留一半G给P1,所以偷取的数量就是P1本地队列的G总数量-P1本地队列的G总数量/2.如果非常不幸,前面三次随机都没偷到,那么最后一次随机找P偷取的时候,就会不管不顾了,如果目标本地队列没有G,连runnext里面的G也要偷,不然自己就要饿死了,这是兜底的下下策。最后返回的是最后偷到的那个G。

 情况三:runnext为空,local run queue为空,global run queue不为空,此时M1刚执行完G1,切换G0开始寻找下一个可运行的G,此时会去global run queue找G,取的数量是min(sched.runqseze/gomaxprocs + 1,len(p.runq)/2),也就是取两个值中最小的一个。最后放到本地队列。 (去全局队列取G时,主要是要注意不能一次拿太多,毕竟别的P可能也要用,所以这里取最小值,最少都会拿一个,除非没有)

约定:

在创建G时,运行的G会尝试唤醒其他空闲的P和M组合去执行。

借用刘冰丹老师的绘制图。假定G2唤醒M2,M2绑定了P2,并运行G0,但P2本地队列没有G,M2此时为自旋线程(没有可运行的G,不断寻找G)

 自旋线程补充:

G发生系统调用: 

2 源码走读

2.1 重要数据结构

2.1.1 M

M是OS线程的实体。下面是一些重要的字段:

  • 持有用于执行调度器的g0
  • 持有用于处理信号的gsignal
  • 持有线程本地存储tls
  • 持有当前正在运行的curg
  • 持有运行Goroutine时需要的本地资源P
  • 表示自身的自旋和非自旋状态spining
  • 管理在它身上执行的cgo调用
  • 将自己与其他的M进行串联
  • 持有当前线程上进行内存分配的本地缓存mcache
  • M的状态有以下:
  • 1.自旋(spining) :M正在从运行队列获取G,这时候M会拥有一个P
  • 2.执行go代码中:M正在执行go代码,这时候M会有一个P
  • 3.执行原生代码中:M正在执行原生代码(sysmon)或者阻塞的syscall,这是M不拥有P
  • 4.休眠中:M发现无待运行的G时会进入休眠,并添加到空闲的M链表中,这是M不拥有P 
// src/runtime/runtime2.go
type m struct {
	g0          *g			// 用于执行调度指令的 Goroutine
	gsignal     *g			// 处理 signal 的 g
	tls         [6]uintptr	// 线程本地存储
	curg        *g			// 当前运行的用户 Goroutine
	p           puintptr	// 执行 go 代码时持有的 p (如果没有执行则为 nil)
	spinning    bool		// m 当前没有运行 work 且正处于寻找 work 的活跃状态
	cgoCallers  *cgoCallers	// cgo 调用崩溃的 cgo 回溯
	alllink     *m			// 在 allm 上
	mcache      *mcache

	...
}

2.1.2 P 

// src/runtime/runtime2.go
type p struct {
 status      uint32      // p 的状态 pidle/prunning/...
 schedtick   uint32      // 每次执行调度器调度 +1
 syscalltick uint32      // 每次执行系统调用 +1
 m           muintptr    // 关联的 m 
 mcache      *mcache     // 用于 P 所在的线程 M 的内存分配的 mcache
 deferpool    []*_defer  // 本地 P 队列的 defer 结构体池
 // 可运行的 Goroutine 队列,可无锁访问
 runqhead uint32
 runqtail uint32
 runq     [256]guintptr
 // 线程下一个需要执行的 G
 runnext guintptr
 // 空闲的 G 队列,G 状态 status 为 _Gdead,可重新初始化使用
 gFree struct {
  gList
  n int32
 }
        ......
}

最重要的数据结构是status表示P不同的状态,runqhead、runqtail、runq三个字段表示处理器持有的运行队列,是一个长度为256的环形队列,其中存储着待执行的G列表,runnext中是线程下一个要执行的G;gFree存储P本地状态为_Gdead的空闲的G,可重新初始化使用。

P结构体中的状态status字段会是以下5种中的一种:

_Pidle:P没有运行用户代码或者调度器 ,被空闲队列或者改变其状态的结构持有,运行队列为空

_Prunning:被线程M持有,并且正在执行用户代码或者调度器

_Psyscall:没有执行用户代码,当前线程陷入系统调用

_Pgcstop:被线程M持有,当前处理器由于垃圾回收被停止

_Pdead:当前P已经不被使用

下面是P状态切换调用的函数以及不同状态之间的转换

2.1.2 sysmon监控和抢占P 

在程序启动的时候,会创建一个新的线程m,同时创建一个协程g,这个g不需要绑定p,直接与m绑定,也就是所谓的sysmon协程

func main() {
	...
	if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
		// For runtime_syscall_doAllThreadsSyscall, we
		// register sysmon is not ready for the world to be
		// stopped.
		atomic.Store(&sched.sysmonStarting, 1)
		systemstack(func() {
			newm(sysmon, nil, -1)
		})
	}
	...
}

sysmon就是监控协程,抢占retake()的逻辑如下:

const forcePreemptNS = 10 * 1000 * 1000 // 10ms
func retake(now int64) uint32 {
	n := 0
	lock(&allpLock)
	for i := 0; i < len(allp); i++ {
		pp := allp[i]
		if pp == nil {
			continue
		}
		pd := &pp.sysmontick
		s := pp.status
		sysretake := false
		if s == _Prunning || s == _Psyscall {
			// Preempt G if it's running for too long.
			t := int64(pp.schedtick)
			if int64(pd.schedtick) != t {
				pd.schedtick = uint32(t)
				pd.schedwhen = now
			} else if pd.schedwhen+forcePreemptNS <= now {
				// 如果p距离上次调度已经过去了10ms
				// 将p当前执行的goroutine(curg)标记为可以抢占状态
				preemptone(pp)
				sysretake = true
			}
		}
		if s == _Psyscall {
			// 超过了1个监控tick(20us)
			t := int64(pp.syscalltick)
			if !sysretake && int64(pd.syscalltick) != t {
				pd.syscalltick = uint32(t)
				pd.syscallwhen = now
				continue
			}
			if runqempty(pp) && sched.nmspinning.Load()+sched.npidle.Load() > 0 && pd.syscallwhen+10*1000*1000 > now {
				continue
			}
			unlock(&allpLock)
			incidlelocked(-1)
			if atomic.Cas(&pp.status, s, _Pidle) {
				if traceEnabled() {
					traceGoSysBlock(pp)
					traceProcStop(pp)
				}
				n++
				pp.syscalltick++
				handoffp(pp)
			}
			incidlelocked(1)
			lock(&allpLock)
		}
	}
	unlock(&allpLock)
	return uint32(n)
}

 当p在_Prunning或者_Psyscall状态下,以下情况P可能会被抢占:

1.如果距离上次调度已经过去了10ms(pd.schedwhen+forcePreemptNS <= now),也就是某一个G已经执行了10ms

2.如果系统调用超过了一个sysmon tick(20us)

3.系统调用的情况下,p中的本地队列如果还有多余的goroutine,也就是(runqempty(p)不为空)。这个时候抢占是为了让本地队列的其他G有机会执行

4.如果atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 ,说明外面有空闲的p和自旋的m,压根没必要抢占当前的p

如果是情况1,会调用preemptone(),将p当前执行的goroutine(curg)标记为可以抢占的状态:

// 设置p的当前运行goroutine(curg)标记为可抢占
func preemptone(pp *p) bool {
	mp := pp.m.ptr()
	if mp == nil || mp == getg().m {
		return false
	}
	gp := mp.curg
	if gp == nil || gp == mp.g0 {
		return false
	}

	gp.preempt = true

	// Every call in a goroutine checks for stack overflow by
	// comparing the current stack pointer to gp->stackguard0.
	// Setting gp->stackguard0 to StackPreempt folds
	// preemption into the normal stack overflow check.
	gp.stackguard0 = stackPreempt

	// Request an async preemption of this P.
	if preemptMSupported && debug.asyncpreemptoff == 0 {
		pp.preempt = true
		preemptM(mp)
	}

	return true
}

从gp.stackguard0 = stackPreempt上面的注释可以得知,协程调用前都会检查时候会栈溢出,将gp.stackguard0设置为stackguard0,就会让协程进入到检查栈溢出的逻辑中。

检查函数是个汇编函数morestack_noctxt(),函数执行流程:morestack_noctxt() -> morestack() -> newstack()

// src/runtime/stack.go
func newstack() {
	...
	preempt := atomic.Loaduintptr(&gp.stackguard0) == stackPreempt
	...
	if preempt {
        gopreempt_m(gp)
	}
	...
}

最终回到上面的执行逻辑,而gopreempt_m(),会调用goschedImpl(),这就很熟悉了,goschedImpl就是我们主动让出协程的调用函数。

 其他情况:主要就是进入handoffp()函数,只要满足以下条件,就会启动一个m(startm())来接管p:

条件一:p的本地队列或者全局队列不为空

条件二:处理gc

条件三:此时全局没有自旋的m也没有空闲的p,然后尝试启动一个自旋线程

...

接管后,最终就会调用releasem(mp),该方法也执行g.stackguard0=stackPreempt,那么就跟preemptone()执行逻辑一样。

如果上面条件不满足,就通过pidleput()将p放入空闲队列。

func handoffp(pp *p) {
	if !runqempty(pp) || sched.runqsize != 0 {
		startm(pp, false, false)
		return
	}

	if (traceEnabled() || traceShuttingDown()) && traceReaderAvailable() != nil {
		startm(pp, false, false)
		return
	}

	if gcBlackenEnabled != 0 && gcMarkWorkAvailable(pp) {
		startm(pp, false, false)
		return
	}

	if sched.nmspinning.Load()+sched.npidle.Load() == 0 && sched.nmspinning.CompareAndSwap(0, 1) { // TODO: fast atomic
		sched.needspinning.Store(0)
		startm(pp, true, false)
		return
	}
	lock(&sched.lock)
	if sched.gcwaiting.Load() {
		pp.status = _Pgcstop
		sched.stopwait--
		if sched.stopwait == 0 {
			notewakeup(&sched.stopnote)
		}
		unlock(&sched.lock)
		return
	}
	if pp.runSafePointFn != 0 && atomic.Cas(&pp.runSafePointFn, 1, 0) {
		sched.safePointFn(pp)
		sched.safePointWait--
		if sched.safePointWait == 0 {
			notewakeup(&sched.safePointNote)
		}
	}
	if sched.runqsize != 0 {
		unlock(&sched.lock)
		startm(pp, false, false)
		return
	}

	if sched.npidle.Load() == gomaxprocs-1 && sched.lastpoll.Load() != 0 {
		unlock(&sched.lock)
		startm(pp, false, false)
		return
	}

	when := nobarrierWakeTime(pp)
	pidleput(pp, 0)
	unlock(&sched.lock)

	if when != 0 {
		wakeNetPoller(when)
	}
}

 

 2.1.3 G

// src/runtime/runtime2.go
type g struct {
 stack       stack       // 描述了当前 Goroutine 的栈内存范围 [stack.lo, stack.hi)
 stackguard0 uintptr     // 用于调度器抢占式调度
 _panic      *_panic     // 最内侧的 panic 结构体
 _defer      *_defer     // 最内侧的 defer 延迟函数结构体
 m           *m          // 当前 G 占用的线程,可能为空
 sched       gobuf       //  存储 G 的调度相关的数据
 atomicstatus uint32     // G 的状态
 goid         int64      //  G 的 ID
 waitreason   waitReason //当状态status==Gwaiting时等待的原因
 preempt       bool      // 抢占信号
 preemptStop   bool      // 抢占时将状态修改成 `_Gpreempted`
 preemptShrink bool      // 在同步安全点收缩栈
 lockedm        muintptr   //G 被锁定只能在这个 m 上运行
 waiting        *sudog     // 这个 g 当前正在阻塞的 sudog 结构体
 ......
}
  •  stack:描述了当前Goroutine的栈内存范围[stack.lo,stack.hi]
  • stackguard0:可以用于调度器抢占式调度;preempt,preemptStop,preempShrink跟抢占相关
  • defer和panic:分别记录这个G最内侧的panic和_defer结构体
  • m:记录当前G占用线程M,可能为空
  • atmicstatus:表示G的状态
  • sched:存储G的调度相关的数据
  • goid:表示G的ID,对开发者不可见

需要展开描述的是sched字段的runtime.gobuf结构体:

type gobuf struct {
 sp   uintptr      // 栈指针
 pc   uintptr      // 程序计数器,记录G要执行的下一条指令位置
 g    guintptr     // 持有 runtime.gobuf 的 G
 ret  uintptr      // 系统调用的返回值
 ......
}

这些字段会在调度器将当前G切换离开M和调度进入M执行程序时用到,栈指针sp和程序计数器pc用来存放或恢复寄存器中的值,改变程序执行的指令。 

结构体runtime.g的atomicstatus字段存储了当前G的状态,如下:

const (
 // _Gidle 表示 G 刚刚被分配并且还没有被初始化
 _Gidle = iota // 0
 // _Grunnable 表示 G  没有执行代码,没有栈的所有权,存储在运行队列中
 _Grunnable // 1
 // _Grunning 可以执行代码,拥有栈的所有权,被赋予了内核线程 M 和处理器 P
 _Grunning // 2
 // _Gsyscall 正在执行系统调用,拥有栈的所有权,没有执行用户代码,被赋予了内核线程 M 但是不在运行队列上
 _Gsyscall // 3
 // _Gwaiting 由于运行时而被阻塞,没有执行用户代码并且不在运行队列上,但是可能存在于 Channel 的等待队列上
 _Gwaiting // 4
 // _Gdead 没有被使用,没有执行代码,可能有分配的栈
 _Gdead // 6
 // _Gcopystack 栈正在被拷贝,没有执行代码,不在运行队列上
 _Gcopystack // 8
 // _Gpreempted 由于抢占而被阻塞,没有执行用户代码并且不在运行队列上,等待唤醒
 _Gpreempted // 9
 // _Gscan GC 正在扫描栈空间,没有执行代码,可以与其他状态同时存在
 _Gscan          = 0x1000
 ......
)

其中主要的六种状态是:

Gidle:G被创建但还未完全被初始化

Grunnable:当前G为可运行的,正在等待被运行

Grunning:当前的G正在运行

Gsyscall:当前G正在被系统调用

Gwaiting:当前G正在因某个原因而等待

Gdead:当前G完成了运行

 下面是G状态变化图:

2.1.3.1 主动让出 

有时候我们会主动调用runtime.Gosched()让出Goroutine的cpu,比如自旋锁的实现,如果我们发现锁被占用,就直接让出cpu:

type spinLock uint32
func (sl *spinLock) Lock() {
    for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
        runtime.Gosched()
    }
}
func (sl *spinLock) Unlock() {
    atomic.StoreUint32((*uint32)(sl), 0)
}
func NewSpinLock() sync.Locker {
    var lock spinLock
    return &lock
}

// src/runtime/proc.go
func goschedImpl(gp *g) {
	status := readgstatus(gp)
	if status&^_Gscan != _Grunning {
		dumpgstatus(gp)
		throw("bad g status")
	}
	casgstatus(gp, _Grunning, _Grunnable)
	dropg()
	lock(&sched.lock)
	globrunqput(gp)
	unlock(&sched.lock)

	schedule()
}

逻辑如下:

1.将Goroutine的状态从_Grunning变成_Grunnable

 2.通过dropg()将m和Goroutine解绑

3.把Goroutine放入全局队列

4.开启新一轮调度

2.1.3.2 被动等待

这是比较常见的,比如网络I/O、chan阻塞、定时器等都会进入这里

// src/runtime/proc.go
func park_m(gp *g) {
	_g_ := getg()

	if trace.enabled {
		traceGoPark(_g_.m.waittraceev, _g_.m.waittraceskip)
	}

	casgstatus(gp, _Grunning, _Gwaiting)
	dropg()

	if fn := _g_.m.waitunlockf; fn != nil {
		ok := fn(gp, _g_.m.waitlock)
		_g_.m.waitunlockf = nil
		_g_.m.waitlock = nil
		if !ok {
			if trace.enabled {
				traceGoUnpark(gp, 2)
			}
			casgstatus(gp, _Gwaiting, _Grunnable)
			execute(gp, true) // Schedule it back, never returns.
		}
	}
	schedule()
}

func dropg() {
	gp := getg()

	setMNoWB(&gp.m.curg.m, nil)
	setGNoWB(&gp.m.curg, nil)
}
 2.1.3.3 等待被唤醒

运行时通过goready()唤醒等待的goroutine

func goready(gp *g, traceskip int) {
	systemstack(func() {
		ready(gp, traceskip, true)
	})
}

func ready(gp *g, traceskip int, next bool) {
	if traceEnabled() {
		traceGoUnpark(gp, traceskip)
	}

	status := readgstatus(gp)

	// Mark runnable.
	mp := acquirem() // disable preemption because it can be holding p in a local var
	if status&^_Gscan != _Gwaiting {
		dumpgstatus(gp)
		throw("bad g->status in ready")
	}

	// status is Gwaiting or Gscanwaiting, make Grunnable and put on runq
	// 修改g的状态
	casgstatus(gp, _Gwaiting, _Grunnable)
	// 放入p的本地队列,有概率放入runnext
	runqput(mp.p.ptr(), gp, next)
	wakep()
	releasem(mp)
}

主要逻辑:

修改g的状态_Gwaiting->_Grunnable

放入 p的本地队列,有概率放入runnext

2.1.3.4 退出系统调用

系统调用前,运行时会调用reentersyscall().它会完成Goroutine进入系统调用前的准备工作:

func reentersyscall(pc, sp uintptr) {
	gp := getg()

	gp.m.locks++

	gp.stackguard0 = stackPreempt
	gp.throwsplit = true

	// Leave SP around for GC and traceback.
	// 保存当前PC和栈指针SP中的内容
	save(pc, sp)
	gp.syscallsp = sp
	gp.syscallpc = pc
	// 修改g的状态为Gsyscall
	casgstatus(gp, _Grunning, _Gsyscall)
	if staticLockRanking {
		save(pc, sp)
	}
	if gp.syscallsp < gp.stack.lo || gp.stack.hi < gp.syscallsp {
		systemstack(func() {
			print("entersyscall inconsistent ", hex(gp.syscallsp), " [", hex(gp.stack.lo), ",", hex(gp.stack.hi), "]\n")
			throw("entersyscall")
		})
	}

	if traceEnabled() {
		systemstack(traceGoSysCall)
		save(pc, sp)
	}

	if sched.sysmonwait.Load() {
		systemstack(entersyscall_sysmon)
		save(pc, sp)
	}

	if gp.m.p.ptr().runSafePointFn != 0 {
		// runSafePointFn may stack split if run on this stack
		systemstack(runSafePointFn)
		save(pc, sp)
	}

	// 解除m和p之间的绑定
	gp.m.syscalltick = gp.m.p.ptr().syscalltick
	pp := gp.m.p.ptr()
	pp.m = 0
	// 把p放到oldp中,等系统调用结束后,m优先到oldp找p
	gp.m.oldp.set(pp)
	gp.m.p = 0
	atomic.Store(&pp.status, _Psyscall)
	if sched.gcwaiting.Load() {
		systemstack(entersyscall_gcwait)
		save(pc, sp)
	}

	gp.m.locks--
}

1.保存当前PC和栈指针SP中的内容

2.修改g的状态为_Gsyscall;修改p的状态为_Psyscall

3.解除p与m之间的绑定

4.将p放入oldp中(后面结束系统调用使用)

当系统调用结束之后,会调用退出系统调用的函数,将g重新执行

func exitsyscall() {
	gp := getg()

	gp.m.locks++ // see comment in entersyscall
	if getcallersp() > gp.syscallsp {
		throw("exitsyscall: syscall frame is no longer valid")
	}

	gp.waitsince = 0
	// 到oldp找进入系统调用时m绑定的p
	oldp := gp.m.oldp.ptr()
	gp.m.oldp = 0
	// 快路径,也就是原来进入系统调用时存放的p现在空闲,m可以直接绑定oldp
	if exitsyscallfast(oldp) {

		if goroutineProfile.active {
			systemstack(func() {
				tryRecordGoroutineProfileWB(gp)
			})
		}
		if traceEnabled() {
			if oldp != gp.m.p.ptr() || gp.m.syscalltick != gp.m.p.ptr().syscalltick {
				systemstack(traceGoStart)
			}
		}
		gp.m.p.ptr().syscalltick++
		casgstatus(gp, _Gsyscall, _Grunning)

		gp.syscallsp = 0
		gp.m.locks--
		if gp.preempt {
			gp.stackguard0 = stackPreempt
		} else {

			gp.stackguard0 = gp.stack.lo + stackGuard
		}
		gp.throwsplit = false

		if sched.disable.user && !schedEnabled(gp) {
			Gosched()
		}

		return
	}

	if traceEnabled() {
		for oldp != nil && oldp.syscalltick == gp.m.syscalltick {
			osyield()
		}
		gp.trace.sysExitTime = traceClockNow()
	}

	gp.m.locks--

	// Call the scheduler.
	// 慢路经就进入exitsyscall0
	mcall(exitsyscall0)

	gp.syscallsp = 0
	gp.m.p.ptr().syscalltick++
	gp.throwsplit = false
}

 这里有一个快路径和一个慢路径

快路径就是exitsyscallfast(),直接把上面保存的oldp跟当前g的m绑定:

func exitsyscallfast(oldp *p) bool {
	gp := getg()

	if sched.stopwait == freezeStopWait {
		return false
	}

	if oldp != nil && oldp.status == _Psyscall && atomic.Cas(&oldp.status, _Psyscall, _Pidle) {
		wirep(oldp)
		exitsyscallfast_reacquired()
		return true
	}

	if sched.pidle != 0 {
		var ok bool
		systemstack(func() {
			ok = exitsyscallfast_pidle()
			if ok && traceEnabled() {
				if oldp != nil {
					for oldp.syscalltick == gp.m.syscalltick {
						osyield()
					}
				}
				traceGoSysExit()
			}
		})
		if ok {
			return true
		}
	}
	return false
}

func wirep(pp *p) {
	gp := getg()

	if gp.m.p != 0 {
		throw("wirep: already in go")
	}
	if pp.m != 0 || pp.status != _Pidle {
		id := int64(0)
		if pp.m != 0 {
			id = pp.m.ptr().id
		}
		print("wirep: p->m=", pp.m, "(", id, ") p->status=", pp.status, "\n")
		throw("wirep: invalid p state")
	}
	// 将p和m相互绑定
	gp.m.p.set(pp)
	pp.m.set(gp.m)
	// 设置p的状态为pRunning
	pp.status = _Prunning
}

慢路径就是调用exitsyscall0(),把m和g解绑,将g状态变成_Grunnable,看看有没有空闲的P,有就绑定到M然后执行g,没有就把g放到全局链表

func exitsyscall0(gp *g) {
	casgstatus(gp, _Gsyscall, _Grunnable)
	// 解绑M和G
	dropg()
	lock(&sched.lock)
	var pp *p
	if schedEnabled(gp) {
		pp, _ = pidleget(0)
	}
	var locked bool
	if pp == nil {
		// 如果没有空闲的p,就把G放入全局队列allg
		globrunqput(gp)
		locked = gp.lockedm != 0
	} else if sched.sysmonwait.Load() {
		sched.sysmonwait.Store(false)
		notewakeup(&sched.sysmonnote)
	}
	unlock(&sched.lock)
	if pp != nil {
		// 如果有空闲的p,就绑定p和m,然后执行g
		acquirep(pp)
		execute(gp, false) // Never returns.
	}
	if locked {
		stoplockedm()
		execute(gp, false) // Never returns.
	}
	stopm()
	schedule() // Never returns.
}

func dropg() {
	gp := getg()

	setMNoWB(&gp.m.curg.m, nil)
	setGNoWB(&gp.m.curg, nil)
}

 

2.1.4 schedt 

调度器的schedt结构体存储了全局的G队列,空闲M列表和P列表:

// src/runtime/runtime2.go
type schedt struct {
 lock mutex            // schedt的锁
 midle        muintptr // 空闲的M列表
 nmidle       int32    // 空闲的M列表的数量
 nmidlelocked int32    // 被锁定正在工作的M数
 mnext        int64    // 下一个被创建的 M 的 ID
 maxmcount    int32    // 能拥有的最大数量的 M
 pidle      puintptr   // 空闲的 P 链表
 npidle     uint32     // 空闲 P 数量
 nmspinning uint32     // 处于自旋状态的 M 的数量
 // 全局可执行的 G 列表
 runq     gQueue
 runqsize int32        // 全局可执行 G 列表的大小
 // 全局 _Gdead 状态的空闲 G 列表
 gFree struct {
  lock    mutex
  stack   gList // Gs with stacks
  noStack gList // Gs without stacks
  n       int32
 }
 // sudog结构的集中存储
 sudoglock  mutex
 sudogcache *sudog
 // 有效的 defer 结构池
 deferlock mutex
 deferpool *_defer
        ......
}

还有一些全局变量

// src/runtime/runtime2.go
var (
 allm       *m         // 所有的 M
 gomaxprocs int32      // P 的个数,默认为 ncpu 核数
 ncpu       int32
 ......
 sched      schedt     // schedt 全局结构体
 newprocs   int32

 allpLock mutex       // 全局 P 队列的锁
 allp []*p            // 全局 P 队列,个数为 gomaxprocs
        ......
}

 2.2 程序启动流程

从简单的hello world程序开始追寻,借助dlv来进行调试,在linux下进行。

package main

import "fmt"

func main() {
    fmt.Println("hello world")
}

先构建
 

go build main.go

dlv exec ./main

 然后按照下面步骤执行

由上可知linux amd64系统的启动函数是在asm_amd64.s的runtime.rt0_go函数中。当然,不同的平台有不同的程序入口,有兴趣的可以自行了解。(我看不懂汇编,下面是别人博客的注解)

TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
 ......
 // 初始化g0
 MOVD $runtime·g0(SB), g
        ......
 // 初始化 m0
 MOVD $runtime·m0(SB), R0
// 绑定 g0 和 m0
 MOVD g, m_g0(R0)
 MOVD R0, g_m(g)
        ......
 BL runtime·schedinit(SB)      // 调度器初始化

 // 创建一个新的 goroutine 来启动程序
 MOVD $runtime·mainPC(SB), R0    // main函数入口 
 .......
 BL runtime·newproc(SB)        // 负责根据主函数即 main 的入口地址创建可被运行时调度的执行单元goroutine
 .......

 // 开始启动调度器的调度循环
 BL runtime·mstart(SB)
 ......

DATA runtime·mainPC+0(SB)/8,$runtime·main<ABIInternal>(SB)    // main函数入口地址
GLOBL runtime·mainPC(SB),RODATA,$8

 Go程序的真正启动函数runtime.rt0_go主要做了如下几件事:

(1)初始化g0和m0,并将两者相互绑定,m0是程序启动后的初始线程,g0是m0的系统栈代表的G结构体,负责普通G在M上的调度切换;

(2)schedinit:进行各种运行时组件的初始化工作,这就包括调度器与内存分配器、回收器的初始化,这个函数主要关注mcommoninit()和Procresize()这两个函数是如何初始化M的allm和P的allp;

(3)newproc:负责根据主函数即main的入口地址创建可被运行时调度的执行单元(也就是创建G),这个函数主要关心创建一个新G干了啥

(4)mstart:开始启动调度器的调度循环

Go程序启动后的调度器主要逻辑如下:

主要分两部分来走读源码:调度器的启动和调度循环

2.2.1 调度器的启动

调度器启动函数在 src/runtime/proc.go包的schedinit()函数:

// 调度器初始化
func schedinit() {
	...
	gp := getg()
	if raceenabled {
		gp.racectx, raceprocctx0 = raceinit()
	}

	// 设置机器线程数M最大为10000
	sched.maxmcount = 10000

	// The world starts stopped.
	worldStopped()

	moduledataverify()
	// 栈、内存分配器相关初始化
	stackinit()
	mallocinit()
    ...
	// 初始化当前系统线程M0,添加到全局链表allm
	mcommoninit(gp.m, -1)
    ...
	// GC初始化
	gcinit()

	...

	lock(&sched.lock)
	sched.lastpoll.Store(nanotime())
	// 设置p的值为GOMAXPROCS个数
	procs := ncpu
	if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
		procs = n
	}
	// procresize()调整p列表,将p0和m0绑定
	if procresize(procs) != nil {
		throw("unknown runnable goroutine during bootstrap")
	}
	...
}

schedinit()函数会设置M最大数量为10000,实际中不会达到;会分别调用stackinit()、mallocinit() 、mcommoninit()、gcinit()等执行goroutine栈初始化、进行内存分配器初始化、进行系统线程M0初始化、进行GC垃圾回收器的初始化;接着,将P个数设置为GOMAXPROCS的值,即程序能够同时运行的最大处理器数,最后会调用runtime.procresize()函数初始化P列表。

mcommoninit()函数主要负责对M0和M资源池(allm)进行一个初步的初始化,并将其添加到schedt全局结构体中,这里访问全局schedt会加锁:

// 主要负责对M0进行一个初步的初始化,并将其添加到schedt全局结构体中
func mcommoninit(mp *m, id int64) {
	gp := getg()

	...

	lock(&sched.lock)

	if id >= 0 {
		mp.id = id
	} else {
		mp.id = mReserveID()
	}

	...

	// Add to allm so garbage collector doesn't free g->m
	// when it is just in a register or thread-local storage.
	// 添加m到全局链表allm中
	mp.alllink = allm

	// NumCgoCall() iterates over allm w/o schedlock,
	// so we need to publish it safely.
	atomicstorep(unsafe.Pointer(&allm), unsafe.Pointer(mp))
	unlock(&sched.lock)

	...
}
2.2.1.1创建P的过程 

runtime.procresize()函数主要就是初始化P资源池(allp),根据用户期望值调整allp的大小。逻辑如下:

func procresize(nprocs int32) *p {
	...
	// 获取先前的p的个数
	old := gomaxprocs
	...

	// 根据runtime.MAXGOPROCS调整p的数量,因为runtime.MAXGOPROCS用户可以自行定义,nprocs就是用户期待的allp的大小
	if nprocs > int32(len(allp)) {
		lock(&allpLock)
		if nprocs <= int32(cap(allp)) {
			// 不需要扩容
			allp = allp[:nprocs]
		} else {
			// 扩容全局p数量
			nallp := make([]*p, nprocs)
			// Copy everything up to allp's cap so we
			// never lose old allocated Ps.
			copy(nallp, allp[:cap(allp)])
			allp = nallp
		}

		...
		unlock(&allpLock)
	}

	// initialize new P's
	// 初始化新的p  -- 上面扩容新建的还未初始化
	for i := old; i < nprocs; i++ {
		pp := allp[i]
		if pp == nil {
			pp = new(p)
		}
		// 初始化p的各种数据
		pp.init(i)
		atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
	}

	gp := getg()
	// p不为空,并且id小于nprocs,那么可以继续使用当前p
	if gp.m.p != 0 && gp.m.p.ptr().id < nprocs {
		// continue to use the current P
		gp.m.p.ptr().status = _Prunning
		gp.m.p.ptr().mcache.prepareForSweep()
	} else {
        ...
		gp.m.p = 0
		pp := allp[0]
		pp.m = 0
		pp.status = _Pidle
		// P0绑定到当前的M0
		acquirep(pp)
        ...
	}

    ...
	// 从未使用的p释放资源
	for i := nprocs; i < old; i++ {
		pp := allp[i]
		pp.destroy()
	}

	// 释放完p重置allp长度
	if int32(len(allp)) != nprocs {
		lock(&allpLock)
		allp = allp[:nprocs]
		idlepMask = idlepMask[:maskWords]
		timerpMask = timerpMask[:maskWords]
		unlock(&allpLock)
	}

	// 将没有本地任务的p放到空闲链表中
	var runnablePs *p
	for i := nprocs - 1; i >= 0; i-- {
		pp := allp[i]
		// 忽略当前正在使用的p
		if gp.m.p.ptr() == pp {
			continue
		}
		// 设置状态为idle
		pp.status = _Pidle
		if runqempty(pp) {
			// p的任务列表为空,放入空闲队列中
			pidleput(pp, now)
		} else {
			// p的任务不为空,获取空闲M绑定到P上
			pp.m.set(mget())
			pp.link.set(runnablePs)
			runnablePs = pp
		}
	}
	stealOrder.reset(uint32(nprocs))
	var int32p *int32 = &gomaxprocs // make compiler check that gomaxprocs is an int32
	atomic.Store((*uint32)(unsafe.Pointer(int32p)), uint32(nprocs))
	if old != nprocs {
		// Notify the limiter that the amount of procs has changed.
		gcCPULimiter.resetCapacity(now, nprocs)
	}
    // 返回包含本地队列的P链表
	return runnablePs
}

 

 runtime.procresize()函数里面调用runtime.p.init初始化新建的P的过程如下:

// 初始化P
func (pp *p) init(id int32) {
	// p的id就是它在allp中的索引
	pp.id = id
	// 新建时设置p状态为pgcstop
	pp.status = _Pgcstop
	pp.sudogcache = pp.sudogbuf[:0]
	pp.deferpool = pp.deferpoolbuf[:0]
	pp.wbBuf.reset()
	// mcache初始化
	if pp.mcache == nil {
		if id == 0 {
			if mcache0 == nil {
				throw("missing mcache?")
			}
			// Use the bootstrap mcache0. Only one P will get
			// mcache0: the one with ID 0.
			pp.mcache = mcache0
		} else {
			pp.mcache = allocmcache()
		}
	}
	if raceenabled && pp.raceprocctx == 0 {
		if id == 0 {
			pp.raceprocctx = raceprocctx0
			raceprocctx0 = 0 // bootstrap
		} else {
			pp.raceprocctx = raceproccreate()
		}
	}
	lockInit(&pp.timersLock, lockRankTimers)

	// This P may get timers when it starts running. Set the mask here
	// since the P may not go through pidleget (notably P 0 on startup).
	timerpMask.set(id)
	// Similarly, we may not go through pidleget before this P starts
	// running if it is P 0 on startup.
	idlepMask.clear(id)
}

这个mcache内存结构原来是在M上的,自从引入P之后,就将该结构体已到了P上,这样就不用每个M维护自己的内存分配mcache,由于P在有M可以执行时才会移动到其他M上去,空闲的M无需分配内存,这种灵活性使得整体现成的内存分配大大减少。

 2.2.1.2 创建G的过程

回到2.2一开始程序启动函数runtime.rt0_go,执行完schedinit()之后,有个动作时调用runtime.newproc函数创建G,runtime.newproc入参是funcval结构体函数,代表go关键字后面调用的函数(也就是初始化过程中执行main函数的Goroutine):

// 创建g,并放入p的运行队列
func newproc(fn *funcval) {
	gp := getg()
	// 获取调用方PC寄存器值,即调用方程序要执行的下一条指令地址
	pc := getcallerpc()
	// 用g0系统栈创建Goroutine对象
	// 传递的参数包括fn函数入口地址,gp(g0),调用方pc
	systemstack(func() {
		// 调用newproc1获取Goroutine结构
		newg := newproc1(fn, gp, pc)

		// 获取当前G的P
		pp := getg().m.p.ptr()
		// 将新的G放入P的本地运行队列
		runqput(pp, newg, true)

		if mainStarted {
			// M启动时唤醒新的P执行G
			wakep()
		}
	})
}

逻辑流程如下图:

 

接着往下看runtime.newproc1()函数的逻辑,主要就是获取或者创建一个状态为_Grunnable的Goroutine:

// 创建一个运行fn函数的goroutine
func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
	...

	// 加锁,禁止这时G的M被抢占
	mp := acquirem() // disable preemption because we hold M and P in local vars.
	// 获取P
	pp := mp.p.ptr()
	// 从p的空闲队列gFree查找空闲G
	newg := gfget(pp)
	if newg == nil {
		// 创建一个栈大小为2kb的G
		newg = malg(stackMin)
		// 更改状态为Gdead
		casgstatus(newg, _Gidle, _Gdead)
		// 将G加入全局allg列表中
		allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
	}
	...

	totalSize := uintptr(4*goarch.PtrSize + sys.MinFrameSize) // extra space in case of reads slightly beyond frame
	totalSize = alignUp(totalSize, sys.StackAlign)
	sp := newg.stack.hi - totalSize
	spArg := sp
	if usesLR {
		// caller's LR
		*(*uintptr)(unsafe.Pointer(sp)) = 0
		prepGoExitFrame(sp)
		spArg += sys.MinFrameSize
	}

	// 初始化G的信息
	memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
	newg.sched.sp = sp
	newg.stktopsp = sp
	newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
	newg.sched.g = guintptr(unsafe.Pointer(newg))
	gostartcallfn(&newg.sched, fn)
	newg.parentGoid = callergp.goid
	newg.gopc = callerpc
	newg.ancestors = saveAncestors(callergp)
	newg.startpc = fn.fn
	if isSystemGoroutine(newg, false) {
		sched.ngsys.Add(1)
	} else {
		// Only user goroutines inherit pprof labels.
		if mp.curg != nil {
			newg.labels = mp.curg.labels
		}
		if goroutineProfile.active {
			newg.goroutineProfiled.Store(goroutineProfileSatisfied)
		}
	}
	// Track initial transition?
	newg.trackingSeq = uint8(fastrand())
	if newg.trackingSeq%gTrackingPeriod == 0 {
		newg.tracking = true
	}
	// 将G的状态CAS为 Gdead -> Grunnable
	casgstatus(newg, _Gdead, _Grunnable)
	gcController.addScannableStack(pp, int64(newg.stack.hi-newg.stack.lo))

	...
	newg.goid = pp.goidcache
	pp.goidcache++
	if raceenabled {
		newg.racectx = racegostart(callerpc)
		newg.raceignore = 0
		if newg.labels != nil {
			racereleasemergeg(newg, unsafe.Pointer(&labelSync))
		}
	}
	if traceEnabled() {
		traceGoCreate(newg, newg.startpc)
	}
	// 释放锁,对应上面的acquirem
	releasem(mp)

	return newg
}

 逻辑图如下:

接着看newproc1()函数是怎么通过gfget()函数获取到已经存在的空闲的Goroutine的,主要就是先看当前P的本地空闲队列gFree有没有存放空闲G,没有就去全局空闲队列sched.gFree里面取32个G,本地和全局都没有就返回空,说明找不到空闲的G复用:

func gfget(pp *p) *g {
retry:
	// 如果P的空闲列表gFree为空,sched的空闲列表gFree不为空
	if pp.gFree.empty() && (!sched.gFree.stack.empty() || !sched.gFree.noStack.empty()) {
		lock(&sched.gFree.lock)
		// Move a batch of free Gs to the P.
		// 从sched的gFree列表中移动32个Goroutine到P的gFree中
		for pp.gFree.n < 32 {
			// Prefer Gs with stacks.
			gp := sched.gFree.stack.pop()
			if gp == nil {
				gp = sched.gFree.noStack.pop()
				if gp == nil {
					break
				}
			}
			sched.gFree.n--
			pp.gFree.push(gp)
			pp.gFree.n++
		}
		unlock(&sched.gFree.lock)
		goto retry
	}
	gp := pp.gFree.pop()
	if gp == nil {
		return nil
	}
	pp.gFree.n--
	if gp.stack.lo != 0 && gp.stack.hi-gp.stack.lo != uintptr(startingStackSize) {
		systemstack(func() {
			stackfree(gp.stack)
			gp.stack.lo = 0
			gp.stack.hi = 0
			gp.stackguard0 = 0
		})
	}
	if gp.stack.lo == 0 {
		// Stack was deallocated in gfput or just above. Allocate a new one.
		systemstack(func() {
			gp.stack = stackalloc(startingStackSize)
		})
		gp.stackguard0 = gp.stack.lo + stackGuard
	} else {
		if raceenabled {
			racemalloc(unsafe.Pointer(gp.stack.lo), gp.stack.hi-gp.stack.lo)
		}
		if msanenabled {
			msanmalloc(unsafe.Pointer(gp.stack.lo), gp.stack.hi-gp.stack.lo)
		}
		if asanenabled {
			asanunpoison(unsafe.Pointer(gp.stack.lo), gp.stack.hi-gp.stack.lo)
		}
	}
	return gp
}

 在runtime.newproc1()函数中,如果不存在空闲的G,就是上面找不到空闲可复用的G,拿会通过runtime.malg()创建一个栈大小为2kb的Goroutine:

// 创建一个新的G结构体
func malg(stacksize int32) *g {
	newg := new(g)
	if stacksize >= 0 {
		// 如果申请的堆栈大小大于0,会通过runtime.stackalloc分配2kb的栈空间
		stacksize = round2(stackSystem + stacksize)
		systemstack(func() {
			newg.stack = stackalloc(uint32(stacksize))
		})
		newg.stackguard0 = newg.stack.lo + stackGuard
		newg.stackguard1 = ^uintptr(0)
		// Clear the bottom word of the stack. We record g
		// there on gsignal stack during VDSO on ARM and ARM64.
		*(*uintptr)(unsafe.Pointer(newg.stack.lo)) = 0
	}
	return newg
}

接着往回看runtime.newproc()函数,执行完newproc1()之后,调用runtime.runqput()函数将G放入P本地队列或者全局队列(有概率会优先被放入runnext):

// 将G放入p的本地运行队列中
func runqput(pp *p, gp *g, next bool) {
	// 保留一定的随机性,不将当前G设置成为P的下一个执行的任务
	if randomizeScheduler && next && fastrandn(2) == 0 {
		next = false
	}

	if next {
	retryNext:
		// 将G放入P的runnext变量中,作为下一个P执行的任务
		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.
		// 获取原来的runnext存储的G,放入P本地运行队列或者全局队列
		gp = oldnext.ptr()
	}

retry:
	h := atomic.LoadAcq(&pp.runqhead) // load-acquire, synchronize with consumers
	t := pp.runqtail
	// 如果p的本地队列没有满,将G放入本地环形队列
	if t-h < uint32(len(pp.runq)) {
		pp.runq[t%uint32(len(pp.runq))].set(gp)
		atomic.StoreRel(&pp.runqtail, t+1) // store-release, makes the item available for consumption
		return
	}
	// 如果P的本地队列满了,就把G放入全局队列
	if runqputslow(pp, gp, h, t) {
		return
	}
	// the queue is not full, now the put above must succeed
	goto retry
}

 

接着看如果本地队列满了,调用runqputslow()会发生什么:

// 将G和P本地队列的一部分放入全局队列
func runqputslow(pp *p, gp *g, h, t uint32) bool {
	// 初始化一个本地队列长度一半 + 1的G列表batch
	var batch [len(pp.runq)/2 + 1]*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")
	}
	// 将P的本地环形队列的前一半G放入batch
	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
	}
	// 把新建的G放入列表batch尾部
	batch[n] = gp

	// 打乱batch中存放G的顺序
	if randomizeScheduler {
		for i := uint32(1); i <= n; i++ {
			j := fastrandn(i + 1)
			batch[i], batch[j] = batch[j], batch[i]
		}
	}

	// Link the goroutines.
	// 将batch中的G连成一个链表
	for i := uint32(0); i < n; i++ {
		batch[i].schedlink.set(batch[i+1])
	}
	// 把batch列表设置成gQueue队列
	var q gQueue
	q.head.set(batch[0])
	q.tail.set(batch[n])

	// Now put the batch on global queue.
	// 把gQueue队列放入全局队列
	lock(&sched.lock)
	globrunqputbatch(&q, int32(n+1))
	unlock(&sched.lock)
	return true
}

 主要就是当前p的本地环形队列的前一半和新建的G打乱顺序放入全局链表

最后,newproc()还剩最后一个wakep()函数,该函数就是1.1.5提到的,新建G时唤醒P去找M组合自旋,然后寻找可执行的Goroutine:

func wakep() {
    ...
	mp := acquirem()

	var pp *p
	lock(&sched.lock)
	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)
}

func acquirem() *m {
	gp := getg()
	gp.m.locks++
	return gp.m
}

func releasem(mp *m) {
	gp := getg()
	mp.locks--
	if mp.locks == 0 && gp.preempt {
		gp.stackguard0 = stackPreempt
	}
}

func startm(pp *p, spinning, lockheld bool) {
	mp := acquirem()
	if !lockheld {
		lock(&sched.lock)
	}
	if pp == nil {
		if spinning {
			throw("startm: P required for spinning=true")
		}
		pp, _ = pidleget(0)
		if pp == nil {
			if !lockheld {
				unlock(&sched.lock)
			}
			releasem(mp)
			return
		}
	}
	nmp := mget()
	if nmp == nil {
		id := mReserveID()
		unlock(&sched.lock)

		var fn func()
		if spinning {
			fn = mspinning
		}
		newm(fn, pp, id)

		if lockheld {
			lock(&sched.lock)
		}
		releasem(mp)
		return
	}
	if !lockheld {
		unlock(&sched.lock)
	}
	...
	nmp.spinning = spinning
	nmp.nextp.set(pp)
	notewakeup(&nmp.park)
	releasem(mp)
}
2.2.1.3 小结 

整个运行链条:mcommoninit -> procresize -> newproc

在调度器初始化过程中,首先通过mcommoninit对M的信号进行初始化。然后通过procresize创建与CPU核心数(或者用户指定的GOMAXPROCS)相同的P。最后通过newproc创建包含可以运行要执行函数的执行栈、运行现场的G,并将创建的G放入刚创建好的P的本地可执行队列(第一个入队的G,也就是要执行main函数的Goroutine)

调度器的设计还是挺牛逼,通过引入了一个P,巧妙的缓解了全局锁的调用频率,进一步压榨机器的性能。Goroutine本身也没有想象的那样花里胡哨,运行时只是将其作为一个需要运行的地址保存在了G中,同时对调用的参数进行了一份拷贝。重要的其实还是M,它才是执行代码的真身。

2.2.2 调度循环

所有的初始化过程上面都已经完成了,是时候启动运行时调度器了。我们已经知道,当所有工作准备完成的时候,最后一个开始执行引导调用就是runtime.mstart()函数。(这个runtime.mstart()时汇编写的,脑壳痛,我看不懂,但是它实际上是调用了mstart0())

TEXT runtime·rt0_go(SB),NOSPLIT,$0
	(...)
	CALL	runtime·newproc(SB) // G 初始化
	POPQ	AX
	POPQ	AX

	// 启动 M
	CALL	runtime·mstart(SB) // 开始执行
	RET

DATA	runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOBL	runtime·mainPC(SB),RODATA,$8
TEXT runtime·mstart(SB),NOSPLIT|TOPFRAME,$0
 CALL runtime·mstart0(SB)
 RET // not reached
2.2.2.1 执行前的准备

runtime.mastart0()主要就是初始化G的栈边界,也就是在开始前要计算栈边界,确认栈边界的一些作用,然后才能执行mstart1().

func mstart0() {
	gp := getg()

	osStack := gp.stack.lo == 0
	if osStack {
		size := gp.stack.hi
		if size == 0 {
			size = 16384 * sys.StackGuardMultiplier
		}
		gp.stack.hi = uintptr(noescape(unsafe.Pointer(&size)))
		gp.stack.lo = gp.stack.hi - size + 1024
	}
	// 初始化g0的参数
	gp.stackguard0 = gp.stack.lo + stackGuard
	// This is the g0, so we can also call go:systemstack
	// functions, which check stackguard1.
	gp.stackguard1 = gp.stackguard0
	mstart1()

	// Exit this thread.
	if mStackIsSystemAllocated() {
		osStack = true
	}
	mexit(osStack)
}

再接着看mstart1()

func mstart1() {
	gp := getg()

	if gp != gp.m.g0 {
		throw("bad runtime·mstart")
	}

	// 记录当前栈帧,便于其他调用复用,当进入schedule之后,再也不会回到mstart1
	gp.sched.g = guintptr(unsafe.Pointer(gp))
	gp.sched.pc = getcallerpc()
	gp.sched.sp = getcallersp()

	asminit()
	minit()
	// 设置信号handler;在minit之后,因为minit可以准备处理信号的线程
	if gp.m == &m0 {
		mstartm0()
	}

	// 执行启动函数
	if fn := gp.m.mstartfn; fn != nil {
		fn()
	}

	// 如果当前m并非m0(m0已经跟p0绑定),则要求绑定p
	if gp.m != &m0 {
		acquirep(gp.m.nextp.ptr())
		gp.m.nextp = 0
	}
	// 准备好后,开始调度循环,永不返回
	schedule()
}
2.2.2.2 核心调度

runtime.schedule()函数的逻辑是:

func schedule() {
	mp := getg().m

	if mp.locks != 0 {
		throw("schedule: holding locks")
	}

	if mp.lockedg != 0 {
		stoplockedm()
		execute(mp.lockedg.ptr(), false) // Never returns.
	}

	if mp.incgo {
		throw("schedule: in cgo")
	}

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

	// 安全检查,如果G所在的M处于自旋状态,那么P的运行队列为空
	if mp.spinning && (pp.runnext != 0 || pp.runqhead != pp.runqtail) {
		throw("schedule: spinning with local work")
	}

	// 阻塞式查找可用的G
	gp, inheritTime, tryWakeP := findRunnable() // blocks until work is available

	if debug.dontfreezetheworld > 0 && freezing.Load() {
		lock(&deadlock)
		lock(&deadlock)
	}

	// M这个时候一定获取到了G,如果M是自旋状态,重置其状态到非自旋
	if mp.spinning {
		resetspinning()
	}

	if sched.disable.user && !schedEnabled(gp) {
		lock(&sched.lock)
		if schedEnabled(gp) {
			unlock(&sched.lock)
		} else {
			sched.disable.runnable.pushBack(gp)
			sched.disable.n++
			unlock(&sched.lock)
			goto top
		}
	}

	if tryWakeP {
		wakep()
	}
	if gp.lockedm != 0 {
		startlockedm(gp)
		goto top
	}

	// 执行G
	execute(gp, inheritTime)
}

接着看这个funRunnable()函数:

func findRunnable() (gp *g, inheritTime, tryWakeP bool) {
	mp := getg().m

top:
	pp := mp.p.ptr()
	if sched.gcwaiting.Load() {
		gcstopm()
		goto top
	}
	if pp.runSafePointFn != 0 {
		runSafePointFn()
	}


	now, pollUntil, _ := checkTimers(pp, 0)


	if traceEnabled() || traceShuttingDown() {
		gp := traceReader()
		if gp != nil {
			casgstatus(gp, _Gwaiting, _Grunnable)
			traceGoUnpark(gp, 0)
			return gp, false, true
		}
	}

	// Try to schedule a GC worker.
	// 如果需要GC,不再进行调度
	if gcBlackenEnabled != 0 {
		gp, tnow := gcController.findRunnableGCWorker(pp, now)
		if gp != nil {
			return gp, false, true
		}
		now = tnow
	}


	// 每调度60次,在61次时就尝试从全局队列取一个G,保证公平性;不然一直处理p的本地队列导致全局队列的G饥饿
	if pp.schedtick%61 == 0 && sched.runqsize > 0 {
		lock(&sched.lock)
		// 从全局队列获取g
		gp := globrunqget(pp, 1)
		unlock(&sched.lock)
		if gp != nil {
			return gp, false, false
		}
	}

	// Wake up the finalizer G.
	if fingStatus.Load()&(fingWait|fingWake) == fingWait|fingWake {
		if gp := wakefing(); gp != nil {
			ready(gp, 0, true)
		}
	}
	if *cgo_yield != nil {
		asmcgocall(*cgo_yield, nil)
	}

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

	// global runq
	// 从全局队列获取一些(min(sched.runqsize/gomaxprocs + 1,len(pp.runq)))G
	if sched.runqsize != 0 {
		lock(&sched.lock)
		gp := globrunqget(pp, 0)
		unlock(&sched.lock)
		if gp != nil {
			return gp, false, false
		}
	}


	// 从netpoll网络轮询器中尝试获取G,优先级比从其他p偷取G要高
	if netpollinited() && netpollWaiters.Load() > 0 && sched.lastpoll.Load() != 0 {
		if list := netpoll(0); !list.empty() { // non-blocking
			gp := list.pop()
			injectglist(&list)
			casgstatus(gp, _Gwaiting, _Grunnable)
			if traceEnabled() {
				traceGoUnpark(gp, 0)
			}
			return gp, false, false
		}
	}


	// 自旋M:从其他P中窃取任务G
	// 限制自旋M数量到忙碌p数量的一半,避免一半p数量、并行机制很慢时的cpu消耗
	if mp.spinning || 2*sched.nmspinning.Load() < gomaxprocs-sched.npidle.Load() {
		if !mp.spinning {
			mp.becomeSpinning()
		}

		// 从其他p或者timer中偷取G
		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
		}
	}

	// 没有任何work可做
	// 如果我们在GC mark阶段,则可以安全的扫描并标记对象为黑色
	// 然后便由work可做,运行idlle-time标记而非直接放弃当前的p
	if gcBlackenEnabled != 0 && gcMarkWorkAvailable(pp) && gcController.addIdleMarkWorker() {
		node := (*gcBgMarkWorkerNode)(gcBgMarkWorkerPool.pop())
		if node != nil {
			pp.gcMarkWorkerMode = gcMarkWorkerIdleMode
			gp := node.gp.ptr()
			casgstatus(gp, _Gwaiting, _Grunnable)
			if traceEnabled() {
				traceGoUnpark(gp, 0)
			}
			return gp, false, false
		}
		gcController.removeIdleMarkWorker()
	}


	gp, otherReady := beforeIdle(now, pollUntil)
	if gp != nil {
		casgstatus(gp, _Gwaiting, _Grunnable)
		if traceEnabled() {
			traceGoUnpark(gp, 0)
		}
		return gp, false, false
	}
	if otherReady {
		goto top
	}


	// 放弃当前的p之前,对allp做一个快照
	allpSnapshot := allp

	idlepMaskSnapshot := idlepMask
	timerpMaskSnapshot := timerpMask

	// return P and block
	// 准备归还p,对调度器加锁
	lock(&sched.lock)
	if sched.gcwaiting.Load() || pp.runSafePointFn != 0 {
		// 进入了gc,回到顶部停止m
		unlock(&sched.lock)
		goto top
	}
	// 如果全局队列中又发现了任务
	if sched.runqsize != 0 {
		// 赶紧偷掉返回
		gp := globrunqget(pp, 0)
		unlock(&sched.lock)
		return gp, false, false
	}
	if !mp.spinning && sched.needspinning.Load() == 1 {
		// See "Delicate dance" comment below.
		mp.becomeSpinning()
		unlock(&sched.lock)
		goto top
	}
	// 归还当前的p
	if releasep() != pp {
		throw("findrunnable: wrong p")
	}
	// 将p放入idle链表
	now = pidleput(pp, now)
	// 解锁,完成归还
	unlock(&sched.lock)


	// 这里要非常小心:线程从自旋到非自旋状态的转换,可能与新Goroutine的提交同时发生
	wasSpinning := mp.spinning
	if mp.spinning {
		// M即将睡眠,状态不再是spinning
		mp.spinning = false
		if sched.nmspinning.Add(-1) < 0 {
			throw("findrunnable: negative nmspinning")
		}

		// 再次检查所有p的runqueue是否有可执行的g
		pp := checkRunqsNoP(allpSnapshot, idlepMaskSnapshot)
		if pp != nil {
			acquirep(pp)
			mp.becomeSpinning()
			goto top
		}

		// Check for idle-priority GC work again.
		// 休眠时再次检查GC work
		pp, gp := checkIdleGCNoP()
		if pp != nil {
			acquirep(pp)
			mp.becomeSpinning()

			// Run the idle worker.
			pp.gcMarkWorkerMode = gcMarkWorkerIdleMode
			casgstatus(gp, _Gwaiting, _Grunnable)
			if traceEnabled() {
				traceGoUnpark(gp, 0)
			}
			return gp, false, false
		}

		pollUntil = checkTimersNoP(allpSnapshot, timerpMaskSnapshot, pollUntil)
	}

	// Poll network until next timer.
	// 休眠前再次检测poll网络
	if netpollinited() && (netpollWaiters.Load() > 0 || pollUntil != 0) && sched.lastpoll.Swap(0) != 0 {
		sched.pollUntil.Store(pollUntil)
		if mp.p != 0 {
			throw("findrunnable: netpoll with p")
		}
		if mp.spinning {
			throw("findrunnable: netpoll with spinning")
		}
		delay := int64(-1)
		if pollUntil != 0 {
			if now == 0 {
				now = nanotime()
			}
			delay = pollUntil - now
			if delay < 0 {
				delay = 0
			}
		}
		if faketime != 0 {
			// When using fake time, just poll.
			delay = 0
		}
		list := netpoll(delay) // block until new work is available
		// Refresh now again, after potentially blocking.
		now = nanotime()
		sched.pollUntil.Store(0)
		sched.lastpoll.Store(now)
		if faketime != 0 && list.empty() {
			// Using fake time and nothing is ready; stop M.
			// When all M's stop, checkdead will call timejump.
			stopm()
			goto top
		}
		lock(&sched.lock)
		pp, _ := pidleget(now)
		unlock(&sched.lock)
		if pp == nil {
			injectglist(&list)
		} else {
			acquirep(pp)
			if !list.empty() {
				gp := list.pop()
				injectglist(&list)
				casgstatus(gp, _Gwaiting, _Grunnable)
				if traceEnabled() {
					traceGoUnpark(gp, 0)
				}
				return gp, false, false
			}
			if wasSpinning {
				mp.becomeSpinning()
			}
			goto top
		}
	} else if pollUntil != 0 && netpollinited() {
		pollerPollUntil := sched.pollUntil.Load()
		if pollerPollUntil == 0 || pollerPollUntil > pollUntil {
			netpollBreak()
		}
	}
	// 休眠当前M
	stopm()
	goto top
}

 这个runtime.findrunnable()函数有点长,主要工作如下:

(1)首先检查是否正在GC,如果是就休眠当前的M

(2)为了保证公平,当全局队列中有待执行的G时,通过schedtick对61取模,意思就是每61次调度时,会有一次从全局的运行队列中查找一个可执行的G,这样可以避免全局队列的G长时间无人调度出现饥饿。

(3)调用runtime.runqget()函数尝试从当前P的本地环形队列中获取_Grunnable的G,获取到就返回,没获取到就执行下面逻辑(下面展开)

(4)调用runtime.globalget()函数尝试从全局链表中获取G,获取到就返回,没获取到就执行下面逻辑

(5)从netpoll网络轮询器中尝试获取G,获取到就返回,没获取到就执行下面逻辑

(6)调用runtime.stealWork()函数尝试从别的P偷取G,获取到就返回,没获取到就执行下面逻辑(下面展开)

(7)检查此时是否是GC mark阶段,如果是,则直接返回mark阶段的G

(8)上面都没找到,准备放弃当前的P,对allp进行快照

(9)对调度器加锁,准备归还P

(10)再次检查全局队列有没有G,有就返回,解锁

(11)此时调度器被锁住,又找不到可执行的G,那就调用releasep()释放当前的p,把m和p解绑,然后把p的状态设置为_Pidle,最后调用pidleput()放回全局空闲列表,然后解锁

(12)因为上面M和P解绑了,所以要把M的状态从自旋切换成非自旋状态。并且减少nmspinning

(13)此时仍然需要重新检查所有的列表,如果在全局队列中发现了G,则直接返回

(14)接着在检查是否是GC mark阶段,如果是,则直接返回mark阶段的G

(15)最后还需要再检查一次netpoll,找到就返回,找不到就休眠当前的M

(16)基于note机制被唤醒之后,M会找空闲的P绑定,然后从新回到(1)接着找G

下面是对上某些函数的展开

2.2.2.2.1 runtime.runqget()从本地队列获取G

优先从runnext中获取,没有就从本地环形队列找

// 从p的本地队列中获取G
func runqget(pp *p) (gp *g, inheritTime bool) {
	// If there's a runnext, it's the next G to run.
	// 如果p有一个runnext,则它就是下一个要执行的g
	next := pp.runnext

	// 如果runnext不为空,而CAS失败,则它有可能被其他p偷取了,
	// 因为其他p可以竞争机会到runnext为0,当前p只能只能设置该字段为非0(只有当前p能把g放到runnext,取的话自己和别的p都可以)
	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
		}
		// 获取头部指针指向的G
		gp := pp.runq[h%uint32(len(pp.runq))].ptr()
		if atomic.CasRel(&pp.runqhead, h, h+1) { // cas-release, commits consume
			return gp, false
		}
	}
}
2.2.2.2.2 runtime.globrunqget()从全局队列获取G

先获取min(sched.runqsize/gomaxprocs + 1,len(pp.runq))个G,两者中取最小的那一个(如果全局队列不为空,最少都会获取一个,最多获取本地队列的一半,防止获取太多别的P获取不到),第一个返回,剩下的放入本地队列

// 从全局队列获取G
func globrunqget(pp *p, max int32) *g {
	assertLockHeld(&sched.lock)

	// 如果全局队列没有G直接返回
	if sched.runqsize == 0 {
		return nil
	}

	// 计算n,表示要从全局队列放入本地队列的G的个数
	n := sched.runqsize/gomaxprocs + 1
	if n > sched.runqsize {
		n = sched.runqsize
	}
	// 不能超过指定要取的最大数量max
	if max > 0 && n > max {
		n = max
	}
	// 计算能不能用本地队列的一半容量存放下n个G,如果放不下就设置n为本地队列的一半
	if n > int32(len(pp.runq))/2 {
		n = int32(len(pp.runq)) / 2
	}

	sched.runqsize -= n

	// 取全局队列的头部G返回
	gp := sched.runq.pop()
	n--
	// 剩下的放进本地队列
	for ; n > 0; n-- {
		gp1 := sched.runq.pop()
		runqput(pp, gp1, false)
	}
	return gp
}
2.2.2.2.3 runtime.stealWork()随机从别的P偷取G

一共尝试四次随机找P偷取,主要是执行runqsteal()函数的runqgrap(),前三次优先P的本地队列偷,要留后一半给P,假设P的本地队列是n个G,所以只偷P本地队列前n - n/2个G,然后返回最后一个偷到的G(为啥返回的是偷到的最后一个G,个人感觉可能是因为第四次有可能是从runnext偷的G,执行的优先级比较高,纯个人看法),剩下的放入本地队列。如果到最后一次,也就是第四次尝试,证明前三次都没偷到,第四次如果P的本地队列没有G,会去P的runnext找

func stealWork(now int64) (gp *g, inheritTime bool, rnow, pollUntil int64, newWork bool) {
	pp := getg().m.p.ptr()

	ranTimer := false

	const stealTries = 4
	for i := 0; i < stealTries; i++ {
		// 最后一次才开始去pp的runnext偷g
		stealTimersOrRunNextG := i == stealTries-1

		for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() {
			...

			// Don't bother to attempt to steal if p2 is idle.
			if !idlepMask.read(enum.position()) {
				if gp := runqsteal(pp, p2, stealTimersOrRunNextG); gp != nil {
					return gp, false, now, pollUntil, ranTimer
				}
			}
		}
	}


	return nil, false, now, pollUntil, ranTimer
}

func runqsteal(pp, p2 *p, stealRunNextG bool) *g {
	t := pp.runqtail
	// 随机遍历p尝试偷取G
	n := runqgrab(p2, &pp.runq, t, stealRunNextG)
	if n == 0 {
		return nil
	}
	n--
	gp := pp.runq[(t+n)%uint32(len(pp.runq))].ptr()
	if n == 0 {
		// 如果只偷了一个,也就是最后一次尝试偷取,从目标的runnext偷到了G,直接返回
		return gp
	}
	h := atomic.LoadAcq(&pp.runqhead) // load-acquire, synchronize with consumers
	if t-h+n >= uint32(len(pp.runq)) {
		throw("runqsteal: runq overflow")
	}
	atomic.StoreRel(&pp.runqtail, t+n) // store-release, makes the item available for consumption
	// 返回最后一个偷盗的G
	return gp
}

func runqgrab(pp *p, batch *[256]guintptr, batchHead uint32, stealRunNextG bool) uint32 {
	for {
		h := atomic.LoadAcq(&pp.runqhead) // load-acquire, synchronize with other consumers
		t := atomic.LoadAcq(&pp.runqtail) // load-acquire, synchronize with the producer
		n := t - h
		// pp为偷取的目标,这里的n表示pp本地队列去掉一半还剩多少(要给pp留一半G)
		n = n - n/2
		if n == 0 {
			// 如果pp的本地队列没有G,转头去偷pp的runnext
			if stealRunNextG {
				// Try to steal from pp.runnext.
				if next := pp.runnext; next != 0 {
					if pp.status == _Prunning {
						if GOOS != "windows" && GOOS != "openbsd" && GOOS != "netbsd" {
							usleep(3)
						} else {
							// On some platforms system timer granularity is
							// 1-15ms, which is way too much for this
							// optimization. So just yield.
							osyield()
						}
					}
					if !pp.runnext.cas(next, 0) {
						continue
					}
					batch[batchHead%uint32(len(batch))] = next
					return 1
				}
			}
			return 0
		}
		if n > uint32(len(pp.runq)/2) { // read inconsistent h and t
			continue
		}
		// 从pp的头部开始偷到n个到本地队列
		for i := uint32(0); i < n; i++ {
			g := pp.runq[(h+i)%uint32(len(pp.runq))]
			batch[(batchHead+i)%uint32(len(batch))] = g
		}
		if atomic.CasRel(&pp.runqhead, h, h+n) { // cas-release, commits consume
			return n
		}
	}
}
2.2.2.2.4 M与P的绑定
func acquirep(pp *p) {
	// Do the part that isn't allowed to have write barriers.
	wirep(pp)

	pp.mcache.prepareForSweep()

	if traceEnabled() {
		traceProcStart()
	}
}

func wirep(pp *p) {
	gp := getg()

	if gp.m.p != 0 {
		throw("wirep: already in go")
	}
	if pp.m != 0 || pp.status != _Pidle {
		id := int64(0)
		if pp.m != 0 {
			id = pp.m.ptr().id
		}
		print("wirep: p->m=", pp.m, "(", id, ") p->status=", pp.status, "\n")
		throw("wirep: invalid p state")
	}
	// 将p和m相互绑定
	gp.m.p.set(pp)
	pp.m.set(gp.m)
	// 设置p的状态为pRunning
	pp.status = _Prunning
}
2.2.2.2.5 M的休眠与唤醒
func stopm() {
	gp := getg()
    ...
	lock(&sched.lock)
	// 把M放回全局空闲队列
	mput(gp.m)
	unlock(&sched.lock)
	// 再此阻塞直到被唤醒
	mPark()
	// 重新绑定p
	acquirep(gp.m.nextp.ptr())
	gp.m.nextp = 0
}

// 把M放回全局空闲队列
func mput(mp *m) {
	assertLockHeld(&sched.lock)

	mp.schedlink = sched.midle
	sched.midle.set(mp)
	sched.nmidle++
	checkdead()
}

func mPark() {
	gp := getg()
	// 暂停当前的M,在此处阻塞,直到被唤醒
	notesleep(&gp.m.park)
	// 清除暂停的note
	noteclear(&gp.m.park)
}
2.2.2.2.6 execute()

从上面的findrunnable()找到可执行的G之后,就会进入execute()

func execute(gp *g, inheritTime bool) {
	mp := getg().m

	if goroutineProfile.active {
		tryRecordGoroutineProfile(gp, osyield)
	}

	// 将G绑定到当前的M上
	mp.curg = gp
	gp.m = mp
	// 将g正是切换成Grunnable状态
	casgstatus(gp, _Grunnable, _Grunning)
	gp.waitsince = 0
	gp.preempt = false
	gp.stackguard0 = gp.stack.lo + stackGuard
	if !inheritTime {
		// 调度次数加1
		mp.p.ptr().schedtick++
	}

	// Check whether the profiler needs to be turned on or off.
	hz := sched.profilehz
	if mp.profilehz != hz {
		setThreadCPUProfiler(hz)
	}

	if traceEnabled() {
		// GoSysExit has to happen when we have a P, but before GoStart.
		// So we emit it here.
		if gp.syscallsp != 0 {
			traceGoSysExit()
		}
		traceGoStart()
	}

	// gogo完成从g0到gp真正的切换
	gogo(&gp.sched)
}
2.2.2.2.7 goexit0()

上面其实调用到了gogo()函数,但是是汇编写的,我不太熟悉,大概就是把g0切换成要执行的G,执行完成之后调用runtime.exit(),最后调用的runtime.goexit0(),把G切换回g0,然后清空G的信息,调用gdropg()解除M和G的绑定,然后把G放入当前P的空闲队列gFree中,接着调用schedule()开启新一轮调度循环。

func goexit1() {
	if raceenabled {
		racegoend()
	}
	if traceEnabled() {
		traceGoEnd()
	}
	mcall(goexit0)
}

// goexit continuation on g0.
func goexit0(gp *g) {
	mp := getg().m
	pp := mp.p.ptr()

	// 切换g的状态 Grunning -> Gdead
	casgstatus(gp, _Grunning, _Gdead)
	gcController.addScannableStack(pp, -int64(gp.stack.hi-gp.stack.lo))
	if isSystemGoroutine(gp, false) {
		sched.ngsys.Add(-1)
	}
	// 清理G
	gp.m = nil
	locked := gp.lockedm != 0
	gp.lockedm = 0
	mp.lockedg = 0
	gp.preemptStop = false
	gp.paniconfault = false
	gp._defer = nil // should be true already but just in case.
	gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data.
	gp.writebuf = nil
	gp.waitreason = waitReasonZero
	gp.param = nil
	gp.labels = nil
	gp.timer = nil

	if gcBlackenEnabled != 0 && gp.gcAssistBytes > 0 {
		assistWorkPerByte := gcController.assistWorkPerByte.Load()
		scanCredit := int64(assistWorkPerByte * float64(gp.gcAssistBytes))
		gcController.bgScanCredit.Add(scanCredit)
		gp.gcAssistBytes = 0
	}

	// 解绑M和G
	dropg()

	if GOARCH == "wasm" { // no threads yet on wasm
		// 把G放到pp的gFree空闲队列中
		gfput(pp, gp)
		// 再次调度
		schedule() // never returns
	}

	if mp.lockedInt != 0 {
		print("invalid m->lockedInt = ", mp.lockedInt, "\n")
		throw("internal lockOSThread error")
	}
	gfput(pp, gp)
	if locked {
		if GOOS != "plan9" { // See golang.org/issue/22227.
			gogo(&mp.g0.sched)
		} else {
			// Clear lockedExt on plan9 since we may end up re-using
			// this thread.
			mp.lockedExt = 0
		}
	}
	schedule()
}

3 引用

深入分析Go1.18 GMP调度器底层原理 - 知乎 (zhihu.com)

深入 golang -- GMP调度 - 知乎 (zhihu.com)

详解Go语言调度循环源码实现 - luozhiyun`s Blog

第 6 章 并发调度 | Go 语言原本 (golang.design)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值