typeschedtstruct { lock mutex pidle puintptr// 空闲 p 链表 npidle uint32// 空闲 p 数量 nmspinning uint32// 自旋状态的 M 的数量 runq gQueue// 全局 runnable G 队列 runqsize int32 gFree struct { // 有效 dead G 的全局缓存. lock mutex stack gList// 包含栈的 Gs noStack gList// 没有栈的 Gs n int32 } sudoglock mutex// sudog 结构的集中缓存 sudogcache *sudog deferlock mutex// 不同大小的有效的 defer 结构的池 deferpool [5]*_defer}
M初始化
M 其实就是 OS 线程,它只有两个状态:自旋、非自旋。 在调度器初始化阶段,只有一个 M,那就是主 OS 线程,因此这里的 commoninit 仅仅只是对 M 进行一个初步的初始化, 该初始化包含对 M 及用于处理 M 信号的 G 的相关运算操作,未涉及工作线程的暂止和复始。
// runtime/proc.gofuncmcommoninit(mp *m) { (...)lock(&sched.lock) (...)// mnext 表示当前 m 的数量,还表示下一个 m 的 id mp.id = sched.mnext// 增加 m 的数量 sched.mnext++ (...) // 初始化 gsignal,用于处理 m 上的信号// 添加到 allm 中,从而当它刚保存到寄存器或本地线程存储时候 GC 不会释放 g.m mp.alllink = allm// NumCgoCall() 会在没有使用 schedlock 时遍历 allm,等价于 allm = mpatomicstorep(unsafe.Pointer(&allm), unsafe.Pointer(mp))unlock(&sched.lock) (...)}
p初始化
通常情况下(在程序运行时不调整 P 的个数),P 只会在四种状态下进行切换。 当程序刚开始运行进行初始化时,所有的 P 都处于 _Pgcstop 状态, 随着 P 的初始化(runtime.procresize),会被置于 _Pidle。
当 M 需要运行时,会 runtime.acquirep,并通过 runtime.releasep 来释放。 当 G 执行时需要进入系统调用时,P 会被设置为 _Psyscall, 如果这个时候被系统监控抢夺(runtime.retake),则 P 会被重新修改为 _Pidle。 如果在程序运行中发生 GC,则 P 会被设置为 _Pgcstop, 并在 runtime.startTheWorld 时重新调整为 _Pidle 或者 _Prunning。
funcprocresize(nprocs int32) *p {// 获取先前的 P 个数 old := gomaxprocs (...)// 更新统计信息,记录此次修改 gomaxprocs 的时间 now :=nanotime()if sched.procresizetime !=0 { sched.totaltime +=int64(old) * (now - sched.procresizetime) } sched.procresizetime = now// 必要时增加 allp// 这个时候本质上是在检查用户代码是否有调用过 runtime.MAXGOPROCS 调整 p 的数量// 此处多一步检查是为了避免内部的锁,如果 nprocs 明显小于 allp 的可见数量(因为 len)// 则不需要进行加锁if nprocs >int32(len(allp)) {// 此处与 retake 同步,它可以同时运行,因为它不会在 P 上运行。lock(&allpLock)if nprocs <=int32(cap(allp)) {// 如果 nprocs 被调小了,扔掉多余的 p allp = allp[:nprocs] } else {// 否则(调大了)创建更多的 p nallp :=make([]*p, nprocs)// 将原有的 p 复制到新创建的 new all p 中,不浪费旧的 pcopy(nallp, allp[:cap(allp)]) allp = nallp }unlock(&allpLock) }// 初始化新的 Pfor i := old; i < nprocs; i++ { pp := allp[i]// 如果 p 是新创建的(新创建的 p 在数组中为 nil),则申请新的 P 对象if pp ==nil { pp =new(p) } pp.init(i)atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp)) // allp[i] = pp } _g_ :=getg()// 如果当前正在使用的 P 应该被释放,则更换为 allp[0]// 否则是初始化阶段,没有 P 绑定当前 P allp[0]if _g_.m.p !=0&& _g_.m.p.ptr().id < nprocs {// 继续使用当前 P _g_.m.p.ptr().status = _Prunning (...) } else {// 释放当前 P,因为已失效if _g_.m.p !=0 { _g_.m.p.ptr().m =0 } _g_.m.p =0 _g_.m.mcache =nil// 更换到 allp[0] p := allp[0] p.m =0 p.status = _Pidleacquirep(p) // 直接将 allp[0] 绑定到当前的 M (...) }// 从未使用的 p 释放资源for i := nprocs; i < old; i++ { p := allp[i] p.destroy()// 不能释放 p 本身,因为他可能在 m 进入系统调用时被引用 }// 清理完毕后,修剪 allp, nprocs 个数之外的所有 Pifint32(len(allp)) != nprocs {lock(&allpLock) allp = allp[:nprocs]unlock(&allpLock) }// 将没有本地任务的 P 放到空闲链表中var runnablePs *pfor i := nprocs -1; i >=0; i-- {// 挨个检查 p p := allp[i]// 确保不是当前正在使用的 Pif _g_.m.p.ptr() == p {continue }// 将 p 设为 idel p.status = _Pidleifrunqempty(p) {// 放入 idle 链表pidleput(p) } else {// 如果有本地任务,则为其绑定一个 M p.m.set(mget())// 第一个循环为 nil,后续则为上一个 p// 此处即为构建可运行的 p 链表 p.link.set(runnablePs) runnablePs = p } } stealOrder.reset(uint32(nprocs)) atomic.Store((*uint32)(unsafe.Pointer(gomaxprocs)), uint32(nprocs)) // gomaxprocs = nprocsreturn runnablePs // 返回所有包含本地任务的 P 链表}// 初始化 pp,func (pp *p) init(id int32) {// p 的 id 就是它在 allp 中的索引 pp.id = id// 新创建的 p 处于 _Pgcstop 状态 pp.status = _Pgcstop (...)// 为 P 分配 cache 对象if pp.mcache ==nil {// 如果 old == 0 且 i == 0 说明这是引导阶段初始化第一个 pif id ==0 { (...) pp.mcache =getg().m.mcache // bootstrap } else { pp.mcache =allocmcache() } } (...)}// 释放未使用的 P,一般情况下不会执行这段代码func (pp *p) destroy() {// 将所有 runnable Goroutine 移动至全局队列for pp.runqhead != pp.runqtail {// 从本地队列中 pop pp.runqtail-- gp := pp.runq[pp.runqtail%uint32(len(pp.runq))].ptr()// push 到全局队列中globrunqputhead(gp) }if pp.runnext !=0 {globrunqputhead(pp.runnext.ptr()) pp.runnext =0 } (...)// 将当前 P 的空闲的 G 复链转移到全局gfpurge(pp) (...) pp.status = _Pdead}
procresize 这个函数相对较长,我们来总结一下它主要干了什么事情:
调用时已经 STW,记录调整 P 的时间; 按需调整 allp 的大小; 按需初始化 allp 中的 P; 如果当前的 P 还可以继续使用(没有被移除),则将 P 设置为 _Prunning; 否则将第一个 P 抢过来给当前 G 的 M 进行绑定 从 allp 移除不需要的 P,将释放的 P 队列中的任务扔进全局队列; 最后挨个检查 P,将没有任务的 P 放入 idle 队列 除去当前 P 之外,将有任务的 P 彼此串联成链表,将没有任务的 P 放回到 idle 链表中 显然,在运行 P 初始化之前,我们刚刚初始化完 M,因此第 7 步中的绑定 M 会将当前的 P 绑定到初始 M 上。 而后由于程序刚刚开始,P 队列是空的,所以他们都会被链接到可运行的 P 链表上处于 _Pidle 状态。