go调度器~支离破碎篇

ready

if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 { // TODO: fast atomic
	wakep()
}

标记G可以运行(_Grunnable)时,如果发现:

  • 没有空闲Pnpidle==0),证明所有P都处于运行或者待运行状态,说明M可能资源紧缺
  • 没有空闲Mnmspinning==0),这个在proc.go的开头有介绍,当worker thread执行完当前任务,本地PG队列以及全局G队列都领不到任务,证明没有可用的操作系统级线程

当前的P是最后一个空闲的,需要新增M,但这里的方法名是wakep,方法体内部实际做的工作:

  • 这时,如果npidle>0证明有空闲的P,新得到的M就直接把这个P领走,分摊现有M的压力,如果没有空闲下来的P,这里就不试图新增M。也就是说在ready中发现可能需要新增M,但是在wakeup时,有发现没有能够领取的P,就先不新增,防止M新增后,进入park状态,增加系统开销,这里需要保证的是,新增M就要立即分摊压力。PM的组合是运行G的基础,后续的sched会把新增G分摊到这个P
  • 打包空闲P和可能新增的M,新增一个分摊G的单元

这里有个疑惑的点notewakeup(&mp.park),这个开始以为是试图激活等待在park这个note的某些M但发现并没有针对park的sleep调用。这里我查看了雨痕的注释,仅仅说是“唤醒M”。注意上面黄色区域是错的,牛人一般不应该做没有意义的wakeup,在stopm的时候会将M放回idle队列,并且sleep在park上,而startm开始会先从idle的M队列中获取M,所以M存在sleep的可能性。不过新创建的M并没有分配P给他,所以应该没有sleep的可能性。

gcprocs

只有在mgc.go中调用,给gc分配的m的数量,优先级从低到高

  • cpu的数量
  • go运行库允许的gc线程的上限-32个
  • 最高优先级,需要考虑到系统的实际运行情况,调用gcprocsm和当前进程空闲m的数量,gc会尽量不抢夺资源,透明运行

needaddgcproc

gcprocs的衍生方法,取当前进程还可以给gc增加多少m

helpgc

这里给所有p通过mget绑定m,并激活m去运行。

这里有个问题,为什么这里可以直接使用全局p链表,而不考虑并发问题,追溯到该方法的调用,应该是在STW之后,当前属于gc mark时间,所有资源都为gc服务。

forEachP

proc.go中一些操作需要所有p执行某个操作,或者达到某个状态,整体协调的是类似以下的变量:

safePointFn   func(*p) // 操作
safePointWait int32 // 需要执行的总次数,p的数量
safePointNote note // 主要用于暂停一段时间,不是所有的p都能马上被抢占

方法的实现细节在proc.go都有比较相似的地方,初次看可能比较费劲,熟悉作者的代码风格后会相对好点。

支离破碎的读了一些函数,试图领会其中的关键点,不过领悟到的东西捉襟见肘啊

runqP中负责存储G的队列,结构是队列,大小256,多余的G会放到全局的队列中,可能是为了更好的在多核上分摊任务

runnext是这里需要着重理解的变量,每个G像真正的thread一样有运行时间片,runnext指向的G优先于runq中所有G,源码中对于这个变量有下面一段注释比较难理解:

// If a set of goroutines is locked in a
// communicate-and-wait pattern, this schedules that set as a
// unit and eliminates the (potentially large) scheduling
// latency that otherwise arises from adding the ready'd
// goroutines to the end of the run queue.

这里意思是“如果有一个goroutine的集合都被锁住了,那么runnext会消除当这个集合被唤醒时带来的大量入队请求”,所以需要找到这个场景发生的代码。带着这个问题阅读剩下的代码。。。

runq这里还有个技术点,是关于并发的,拿runqempty举例:

for {
	head := atomic.Load(&_p_.runqhead)
	tail := atomic.Load(&_p_.runqtail)
	runnext := atomic.Loaduintptr((*uintptr)(unsafe.Pointer(&_p_.runnext)))
	if tail == atomic.Load(&_p_.runqtail) {
		return head == tail && runnext == 0
	}
}

对于指针的获取都是通过atomic库操作的,这能保证拿到最近的值,但阻止不了其他协程修改_p_.runqhead这样的属性,也就是说这里会尽量保证判断出真实的empty状况。这个对于用到这些方法的逻辑的影响,以及为什么这样设计,就是我需要带着的第二个问题。。。

acquirepreleasep,这两个方法的重点不在于怎么关联或者去除PM的关系,而在于:

  • 什么情况下不能做这个动作
  • 应用场景
if _g_.m.p != 0 || _g_.m.mcache != nil {

acquireq从上面的判断来看,Mpmcache可以推断是否正在运行中,Pmstatus字段可以推断是否可以拆除P

if _g_.m.p == 0 || _g_.m.mcache == nil {

releasep M可以和上面的条件比较学习,不过对于P的判断条件有些不同,首先需要PM是有绑定关系的(_p_.m.ptr() != _g_.m || _p_.mcache != _g_.m.mcache),其次就是在对于状态也有要求。

gcount获取当前运行的上层app发起G的数量

不知道量变会不会引起质变,这篇blog,纯属流水账,四面放枪

go调度器~支离破碎篇
Share this