1. goroutine源码分析

1.1 初始化

  go程序的启动流程分为四步

  1. call osinit, 这里就是设置了全局变量ncpu = cpu核心数量
  2. call schedinit
  3. make & queue new G (runtime.newproc, go func()也是调用这个函数来创建goroutine)
  4. call runtime·mstart

  其中,schedinit 就是调度器的初始化,除了schedinit 中对内存分配,垃圾回收等操作,针对调度器的初始化大致就是初始化自身,设置最大的maxmcount, 确定p的数量并初始化这些操作。

schedinit

  schedinit这里对当前m进行了初始化,并根据osinit获取到的CPU核数和设置的GOMAXPROCS确定P的数量,并进行初始化。

 func schedinit() {
// 从TLS或者专用寄存器获取当前g的指针类型
_g_ := getg()
// 设置m最大的数量
sched.maxmcount = // 初始化栈的复用空间
stackinit()
// 初始化当前m
mcommoninit(_g_.m) // osinit的时候会设置 ncpu这个全局变量,这里就是根据cpu核心数和参数GOMAXPROCS来确定p的数量
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > {
procs = n
}
// 生成设定数量的p
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}
}

  初始化当前M时调用了 mcommoninit() 函数,再看下这个函数的实现

mcommoninit

 func mcommoninit(mp *m) {
_g_ := getg() lock(&sched.lock)
// 判断mnext的值是否溢出,mnext需要赋值给m.id
if sched.mnext+ < sched.mnext {
throw("runtime: thread ID overflow")
}
mp.id = sched.mnext
sched.mnext++
// 判断m的数量是否比maxmcount设定的要多,如果超出直接报异常
checkmcount()
// 创建一个新的g用于处理signal,并分配栈
mpreinit(mp)
if mp.gsignal != nil {
mp.gsignal.stackguard1 = mp.gsignal.stack.lo + _StackGuard
} //添加到allm,以便垃圾收集器不会释放g-> m
    //仅在寄存器或线程本地存储中时。 // 接下来的两行,首先将当前m放到allm的头,然后原子操作,将当前m的地址,赋值给m,这样就将当前m添加到了allm链表的头了
mp.alllink = allm // NumCgoCall()遍历不带schedlock的allm,
    //,因此我们需要安全地发布它。
atomicstorep(unsafe.Pointer(&allm), unsafe.Pointer(mp))
unlock(&sched.lock) //如果cgo调用崩溃,则分配内存以保留cgo追溯。
if iscgo || GOOS == "solaris" || GOOS == "windows" {
mp.cgoCallers = new(cgoCallers)
}
}

  在这里就开始涉及到了m链表了,这个链表可以如下图表示:

  再来看一下生成P的函数procesize:

procresize

  这个函数可以改变p的数量,多退少补的原则,在初始化过程中,由于最开始是没有p的,所以开始的作用就是初始化设定数量的p。procresize不仅在初始化的时候会被调用,当用户手动调用runtime.GOMAXPROCS 的时候,会重新设定 nprocs,然后执行 startTheWorld(), startTheWorld()会是使用新的 nprocs 再次调用procresize 这个方法。

 func procresize(nprocs int32) *p {
old := gomaxprocs
if old < || nprocs <= {
throw("procresize: invalid arg")
}
// 更新统计
now := nanotime()
if sched.procresizetime != {
sched.totaltime += int64(old) * (now - sched.procresizetime)
}
sched.procresizetime = now // Grow allp if necessary.
// 如果新给的p的数量比原先的p的数量多,则新建增长的p
if nprocs > int32(len(allp)) {
// 与取录同步(可能正在运行)同时运行,因为它不在P上运行。
lock(&allpLock)
// 判断allp 的cap是否满足增长后的长度,满足就直接使用,不满足,则需要扩张这个slice
if nprocs <= int32(cap(allp)) {
allp = allp[:nprocs]
} else {
nallp := make([]*p, nprocs)
//复制所有内容至allp的上限,因此我们永远不会丢失旧分配的P。
copy(nallp, allp[:cap(allp)])
allp = nallp
}
unlock(&allpLock)
} // initialize new P's
// 初始化新增的p
for i := int32(); i < nprocs; i++ {
pp := allp[i]
if pp == nil {
pp = new(p)
pp.id = i
pp.status = _Pgcstop
pp.sudogcache = pp.sudogbuf[:]
for i := range pp.deferpool {
pp.deferpool[i] = pp.deferpoolbuf[i][:]
}
pp.wbBuf.reset()
// allp是一个slice,直接将新增的p放到对应的索引下面就ok了
atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
}
if pp.mcache == nil {
// 初始化时,old=0,第一个新建的p给当前的m使用
if old == && i == {
if getg().m.mcache == nil {
throw("missing mcache?")
}
pp.mcache = getg().m.mcache // bootstrap
} else {
// 为p分配内存
pp.mcache = allocmcache()
}
}
} // free unused P's
// 释放掉多余的p,当新设置的p的数量,比原先设定的p的数量少的时候,会走到这个流程
// 通过 runtime.GOMAXPROCS 就可以动态的修改nprocs
for i := nprocs; i < old; i++ {
p := allp[i]
// move all runnable goroutines to the global queue
// 把当前p的运行队列里的g转移到全局的g的队列
for p.runqhead != p.runqtail {
// pop from tail of local queue
p.runqtail--
gp := p.runq[p.runqtail%uint32(len(p.runq))].ptr()
// push onto head of global queue
globrunqputhead(gp)
}
// 把runnext里的g也转移到全局队列
if p.runnext != {
globrunqputhead(p.runnext.ptr())
p.runnext =
}
// if there's a background worker, make it runnable and put
// it on the global queue so it can clean itself up
// 如果有gc worker的话,修改g的状态,然后再把它放到全局队列中
if gp := p.gcBgMarkWorker.ptr(); gp != nil {
casgstatus(gp, _Gwaiting, _Grunnable)
globrunqput(gp)
// This assignment doesn't race because the
// world is stopped.
p.gcBgMarkWorker.set(nil)
}
// sudoig的buf和cache,以及deferpool全部清空
for i := range p.sudogbuf {
p.sudogbuf[i] = nil
}
p.sudogcache = p.sudogbuf[:]
for i := range p.deferpool {
for j := range p.deferpoolbuf[i] {
p.deferpoolbuf[i][j] = nil
}
p.deferpool[i] = p.deferpoolbuf[i][:]
}
// 释放掉当前p的mcache
freemcache(p.mcache)
p.mcache = nil
// 把当前p的gfree转移到全局
gfpurge(p)
// 修改p的状态,让他自生自灭去了
p.status = _Pdead
// 无法释放P本身,因为它可以被syscall中的M引用
} // Trim allp.
if int32(len(allp)) != nprocs {
lock(&allpLock)
allp = allp[:nprocs]
unlock(&allpLock)
}
// 判断当前g是否有p,有的话更改当前使用的p的状态,继续使用
_g_ := getg()
if _g_.m.p != && _g_.m.p.ptr().id < nprocs {
// continue to use the current P
_g_.m.p.ptr().status = _Prunning
} else {
// release the current P and acquire allp[0]
// 如果当前g有p,但是拥有的是已经释放的p,则不再使用这个p,重新分配
if _g_.m.p != {
_g_.m.p.ptr().m =
}
// 分配allp[0]给当前g使用
_g_.m.p =
_g_.m.mcache = nil
p := allp[]
p.m =
p.status = _Pidle
// 将p m g绑定,并把m.mcache指向p.mcache,并修改p的状态为_Prunning
acquirep(p)
}
var runnablePs *p
for i := nprocs - ; i >= ; i-- {
p := allp[i]
if _g_.m.p.ptr() == p {
continue
}
p.status = _Pidle
// 根据 runqempty 来判断当前p的g运行队列是否为空
if runqempty(p) {
// g运行队列为空的p,放到 sched的pidle队列里面
pidleput(p)
} else {
// g 运行队列不为空的p,组成一个可运行队列,并最后返回
p.m.set(mget())
p.link.set(runnablePs)
runnablePs = p
}
}
stealOrder.reset(uint32(nprocs))
var int32p *int32 = &gomaxprocs // make compiler check that gomaxprocs is an int32
atomic.Store((*uint32)(unsafe.Pointer(int32p)), uint32(nprocs))
return runnablePs
}
  • runqempty: 根据 p.runqtail == p.runqhead 和 p.runnext 来判断有没有待运行的g
  • pidleput: 将当前的p设置为 sched.pidle,然后根据p.link将空闲p串联起来,可参考上图allm的链表示意图

1.2 任务

  只需要使用 go func 就可以创建一个goroutine,编译器会将go func 翻译成 newproc 进行调用,新建的任务是如何调用的呢,下面从创建开始进行源码跟踪

newproc

  newproc 函数获取了参数和当前g的pc信息,并通过g0调用newproc1去真正的执行创建或获取可用的g:

 func newproc(siz int32, fn *funcval) {
// 获取第一参数地址
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
// 获取当前执行的g
gp := getg()
// 获取当前g的pc
pc := getcallerpc()
systemstack(func() {
// 使用g0去执行newproc1函数
newproc1(fn, (*uint8)(argp), siz, gp, pc)
})
}

newproc1

  newporc1 的作用就是创建或者获取一个空的g,并初始化这个g,并尝试寻找一个p和m去执行g。

 func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
_g_ := getg() if fn == nil {
_g_.m.throwing = - // 不要转储完整的堆栈
throw("go of nil func value")
}
// 加锁禁止被抢占
_g_.m.locks++ // 禁用抢占,因为它可以将p保留在本地变量中
siz := narg
siz = (siz + ) &^ // We could allocate a larger initial stack if necessary.
// Not worth it: this is almost always an error.
// 4*sizeof(uintreg): extra space added below
// sizeof(uintreg): caller's LR (arm) or return address (x86, in gostartcall). // 如果参数过多,则直接抛出异常,栈大小是2k
if siz >= _StackMin-*sys.RegSize-sys.RegSize {
throw("newproc: function arguments too large for new goroutine")
} _p_ := _g_.m.p.ptr()
// 尝试获取一个空闲的g,如果获取不到,则新建一个,并添加到allg里面
// gfget首先会尝试从p本地获取空闲的g,如果本地没有的话,则从全局获取一堆平衡到本地p
newg := gfget(_p_)
if newg == nil {
newg = malg(_StackMin)
casgstatus(newg, _Gidle, _Gdead)
// 新建的g,添加到全局的 allg里面,allg是一个slice, append进去即可
allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
}
// 判断获取的g的栈是否正常
if newg.stack.hi == {
throw("newproc1: newg missing stack")
}
// 判断g的状态是否正常
if readgstatus(newg) != _Gdead {
throw("newproc1: new g is not Gdead")
}
// 预留一点空间,防止读取超出一点点
totalSize := *sys.RegSize + uintptr(siz) + sys.MinFrameSize // 多余的空间,以防读取超出框架
// 空间大小进行对齐
totalSize += -totalSize & (sys.SpAlign - ) // align to spAlign
sp := newg.stack.hi - totalSize
spArg := sp
// usesLr 为0,这里不执行
if usesLR {
// caller's LR
*(*uintptr)(unsafe.Pointer(sp)) =
prepGoExitFrame(sp)
spArg += sys.MinFrameSize
}
if narg > {
// 将参数拷贝入栈
memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg))
// ... 省略 ...
}
// 初始化用于保存现场的区域及初始化基本状态
memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
newg.sched.sp = sp
newg.stktopsp = sp
// 这里保存了goexit的地址,在用户函数执行完成后,会根据pc来执行goexit
newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
newg.sched.g = guintptr(unsafe.Pointer(newg))
// 这里调整 sched 信息,pc = goexit的地址
gostartcallfn(&newg.sched, fn)
newg.gopc = callerpc
newg.ancestors = saveAncestors(callergp)
newg.startpc = fn.fn
if _g_.m.curg != nil {
newg.labels = _g_.m.curg.labels
}
if isSystemGoroutine(newg) {
atomic.Xadd(&sched.ngsys, +)
}
newg.gcscanvalid = false
casgstatus(newg, _Gdead, _Grunnable)
// 如果p缓存的goid已经用完,本地再从sched批量获取一点
if _p_.goidcache == _p_.goidcacheend {
/ Sched.goidgen是最后分配的ID,此批次必须为[sched.goidgen + ,sched.goidgen + GoidCacheBatch]。
        //在启动时sched.goidgen = 0,因此主goroutine接收goid = 1。
_p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch)
_p_.goidcache -= _GoidCacheBatch -
_p_.goidcacheend = _p_.goidcache + _GoidCacheBatch
}
// 分配goid
newg.goid = int64(_p_.goidcache)
_p_.goidcache++
// 把新的g放到 p 的可运行g队列中
runqput(_p_, newg, true)
// 判断是否有空闲p,且是否需要唤醒一个m来执行g
if atomic.Load(&sched.npidle) != && atomic.Load(&sched.nmspinning) == && mainStarted {
wakep()
}
_g_.m.locks--
if _g_.m.locks == && _g_.preempt { // 恢复抢占请求,以防我们在新堆栈中清除了它
_g_.stackguard0 = stackPreempt
}
}

gfget

  这个函数就是看一下p有没有空闲的g,没有则去全局的freeg队列查找,这里就涉及了p本地和全局平衡的一个交互了:

 func gfget(_p_ *p) *g {
retry:
gp := _p_.gfree
// 本地的g队列为空,且全局队列不为空,则从全局队列一次获取至多32个下来,如果全局队列不够就算了
if gp == nil && (sched.gfreeStack != nil || sched.gfreeNoStack != nil) {
lock(&sched.gflock)
for _p_.gfreecnt < {
if sched.gfreeStack != nil {
// 优先选择带堆栈的Gs。
gp = sched.gfreeStack
sched.gfreeStack = gp.schedlink.ptr()
} else if sched.gfreeNoStack != nil {
gp = sched.gfreeNoStack
sched.gfreeNoStack = gp.schedlink.ptr()
} else {
break
}
_p_.gfreecnt++
sched.ngfree--
gp.schedlink.set(_p_.gfree)
_p_.gfree = gp
}
// 已经从全局拿了g了,再去从头开始判断
unlock(&sched.gflock)
goto retry
}
// 如果拿到了g,则判断g是否有栈,没有栈就分配
// 栈的分配跟内存分配差不多,首先创建几个固定大小的栈的数组,然后到指定大小的数组里面去分配就ok了,过大则直接全局分配
if gp != nil {
_p_.gfree = gp.schedlink.ptr()
_p_.gfreecnt--
if gp.stack.lo == {
// 堆栈已在gfput中释放,分配一个新的。
systemstack(func() {
gp.stack = stackalloc(_FixedStack)
})
gp.stackguard0 = gp.stack.lo + _StackGuard
} else {
// ... 省略 ...
}
}
// 注意: 如果全局没有g,p也没有g,则返回的gp还是nil
return gp
}

runqput

  runqput会把g放到p的本地队列或者p.runnext,如果p的本地队列过长,则把g到全局队列,同时平衡p本地队列的一半到全局

 func runqput(_p_ *p, gp *g, next bool) {
if randomizeScheduler && next && fastrand()% == {
next = false
}
// 如果next为true,则放入到p.runnext里面,并把原先runnext的g交换出来
if next {
retryNext:
oldnext := _p_.runnext
if !_p_.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) {
goto retryNext
}
if oldnext == {
return
}
// Kick the old runnext out to the regular run queue.
gp = oldnext.ptr()
} retry:
h := atomic.Load(&_p_.runqhead) // load-acquire, synchronize with consumers
t := _p_.runqtail
// 判断p的队列的长度是否超了, runq是一个长度为256的数组,超出的话就会放到全局队列了
if t-h < uint32(len(_p_.runq)) {
_p_.runq[t%uint32(len(_p_.runq))].set(gp)
atomic.Store(&_p_.runqtail, t+) // store-release, makes the item available for consumption
return
}
// 把g放到全局队列
if runqputslow(_p_, gp, h, t) {
return
}
// the queue is not full, now the put above must succeed
goto retry
}

  runqputslow

 func runqputslow(_p_ *p, gp *g, h, t uint32) bool {
var batch [len(_p_.runq)/ + ]*g // 首先,从本地队列中抓取一批。
n := t - h
n = n /
if n != uint32(len(_p_.runq)/) {
throw("runqputslow: queue is not full")
}
// 获取p后面的一半
for i := uint32(); i < n; i++ {
batch[i] = _p_.runq[(h+i)%uint32(len(_p_.runq))].ptr()
}
if !atomic.Cas(&_p_.runqhead, h, h+n) { // cas-release, commits consume
return false
}
batch[n] = gp // 链接goroutines。
for i := uint32(); i < n; i++ {
batch[i].schedlink.set(batch[i+])
} // 现在将批次放入全局队列。
// 放到全局队列队尾
lock(&sched.lock)
globrunqputbatch(batch[], batch[n], int32(n+))
unlock(&sched.lock)
return true
}

  新建任务至此基本结束,创建完成任务后,等待调度执行就好了,从上面可以看出,任务的优先级是 p.runnext > p.runq > sched.runq

  g从创建到执行结束并放入free队列中的状态转换大致如下图所示:

wakep

  当 newproc1创建完任务后,会尝试唤醒m来执行任务

 func wakep() {
// 对旋转线程持保守态度。
// 一次应该只有一个m在spining,否则就退出
if !atomic.Cas(&sched.nmspinning, , ) {
return
}
// 调用startm来执行
startm(nil, true)
}

startm

  调度m或者创建m来运行p,如果p==nil,就会尝试获取一个空闲p,p的队列中有g,拿到p后才能拿到g

 func startm(_p_ *p, spinning bool) {
lock(&sched.lock)
if _p_ == nil {
// 如果没有指定p, 则从sched.pidle获取空闲的p
_p_ = pidleget()
if _p_ == nil {
unlock(&sched.lock)
// 如果没有获取到p,重置nmspinning
if spinning {
// The caller增加了nmspinning,但是没有空闲的Ps,因此,只需取消增量并放弃就可以了。
if int32(atomic.Xadd(&sched.nmspinning, -)) < {
throw("startm: negative nmspinning")
}
}
return
}
}
// 首先尝试从 sched.midle获取一个空闲的m
mp := mget()
unlock(&sched.lock)
if mp == nil {
// 如果获取不到空闲的m,则创建一个 mspining = true的m,并将p绑定到m上,直接返回
var fn func()
if spinning {
// The caller incremented nmspinning, so set m.spinning in the new M.
fn = mspinning
}
newm(fn, _p_)
return
}
// 判断获取到的空闲m是否是spining状态
if mp.spinning {
throw("startm: m is spinning")
}
// 判断获取到的m是否有p
if mp.nextp != {
throw("startm: m has p")
}
if spinning && !runqempty(_p_) {
throw("startm: p has runnable gs")
}
// The caller incremented nmspinning, so set m.spinning in the new M.
// 调用函数的父函数已经增加了nmspinning, 这里只需要设置m.spining就ok了,同时把p绑上来
mp.spinning = spinning
mp.nextp.set(_p_)
// 唤醒m
notewakeup(&mp.park)
}

newm

  newm 通过allocm函数来创建新m

 func newm(fn func(), _p_ *p) {
// 新建一个m
mp := allocm(_p_, fn)
// 为这个新建的m绑定指定的p
mp.nextp.set(_p_)
// ... 省略 ...
// 创建系统线程
newm1(mp)
}

new1m

 func newm1(mp *m) {
// runtime cgo包会把iscgo设置为true,这里不分析
if iscgo {
var ts cgothreadstart
if _cgo_thread_start == nil {
throw("_cgo_thread_start missing")
}
ts.g.set(mp.g0)
ts.tls = (*uint64)(unsafe.Pointer(&mp.tls[]))
ts.fn = unsafe.Pointer(funcPC(mstart))
if msanenabled {
msanwrite(unsafe.Pointer(&ts), unsafe.Sizeof(ts))
}
execLock.rlock() //防止进程克隆。
asmcgocall(_cgo_thread_start, unsafe.Pointer(&ts))
execLock.runlock()
return
}
execLock.rlock() // Prevent process clone.
newosproc(mp)
execLock.runlock()
}

newosproc

  newosproc 创建一个新的系统线程,并执行mstart_stub函数,之后调用mstart函数进入调度,后面在执行流程会分析

 func newosproc(mp *m) {
stk := unsafe.Pointer(mp.g0.stack.hi)
//初始化属性对象。
var attr pthreadattr
var err int32
err = pthread_attr_init(&attr) // 最后,创建线程。它从mstart_stub开始,它执行一些低级操作设置,然后调用mstart。
var oset sigset
sigprocmask(_SIG_SETMASK, &sigset_all, &oset)
// 创建线程,并传入启动启动函数 mstart_stub, mstart_stub 之后调用mstart
err = pthread_create(&attr, funcPC(mstart_stub), unsafe.Pointer(mp))
sigprocmask(_SIG_SETMASK, &oset, nil)
if err != {
write(, unsafe.Pointer(&failthreadcreate[]), int32(len(failthreadcreate)))
exit()
}
}

allocm

  allocm这里首先会释放 sched的freem,然后再去创建m,并初始化m:

 func allocm(_p_ *p, fn func()) *m {
_g_ := getg()
_g_.m.locks++ // 禁用GC,因为可以从sysmon调用它
if _g_.m.p == {
acquirep(_p_) // 在此函数中临时为mallocs借用p
} // 释放免费的M列表。我们需要在某个地方这样做,这样可以释放我们可以使用的堆栈。
// 首先释放掉freem列表
if sched.freem != nil {
lock(&sched.lock)
var newList *m
for freem := sched.freem; freem != nil; {
if freem.freeWait != {
next := freem.freelink
freem.freelink = newList
newList = freem
freem = next
continue
}
stackfree(freem.g0.stack)
freem = freem.freelink
}
sched.freem = newList
unlock(&sched.lock)
} mp := new(m)
// 启动函数,根据startm调用来看,这个fn就是 mspinning, 会将m.mspinning设置为true
mp.mstartfn = fn
// 初始化m,上面已经分析了
mcommoninit(mp)
//如果是cgo或Solaris或Darwin,pthread_create将使我们成为堆栈。
    // Windows和Plan 9将在操作系统堆栈上安排预定的堆栈。
// 为新的m创建g0
if iscgo || GOOS == "solaris" || GOOS == "windows" || GOOS == "plan9" || GOOS == "darwin" {
mp.g0 = malg(-)
} else {
mp.g0 = malg( * sys.StackGuardMultiplier)
}
// 为mp的g0绑定自己
mp.g0.m = mp
// 如果当前的m所绑定的是参数传递过来的p,解除绑定,因为参数传递过来的p稍后要绑定新建的m
if _p_ == _g_.m.p.ptr() {
releasep()
} _g_.m.locks--
if _g_.m.locks == && _g_.preempt { // 恢复抢占请求,以防我们在新堆栈中清除了它
_g_.stackguard0 = stackPreempt
} return mp
}

notewakeup

 func notewakeup(n *note) {
var v uintptr
// 设置m 为locked
for {
v = atomic.Loaduintptr(&n.key)
if atomic.Casuintptr(&n.key, v, locked) {
break
}
} // Successfully set waitm to locked.
// What was it before?
// 根据m的原先的状态,来判断后面的执行流程,0则直接返回,locked则冲突,否则认为是wating,唤醒
switch {
case v == :
// Nothing was waiting. Done.
case v == locked:
// Two notewakeups! Not allowed.
throw("notewakeup - double wakeup")
default:
// Must be the waiting m. Wake it up.
// 唤醒系统线程
semawakeup((*m)(unsafe.Pointer(v)))
}
}

1.3 执行

  在startm函数分析的过程中会,可以看到,有两种获取m的方式

  • 新建: 这时候执行newm1下的newosproc,同时最终调用mstart来执行调度
  • 唤醒空闲m:从休眠的地方继续执行

  m执行g有两个起点,一个是线程启动函数 mstart, 另一个则是休眠被唤醒后的调度schedule了,我们从头开始,也就是mstart, mstart 走到最后也是 schedule 调度。

mstart

 func mstart() {
_g_ := getg() osStack := _g_.stack.lo ==
if osStack {
//从系统堆栈初始化堆栈边界。
         // Cgo可能在stack.hi中具有左堆栈大小。
         // minit可能会更新堆栈边界。 // 从系统堆栈上直接划出所需的范围
size := _g_.stack.hi
if size == {
size = * sys.StackGuardMultiplier
}
_g_.stack.hi = uintptr(noescape(unsafe.Pointer(&size)))
_g_.stack.lo = _g_.stack.hi - size +
}
//初始化堆栈保护,以便我们可以开始调用Go和C函数都具有堆栈增长序言。
_g_.stackguard0 = _g_.stack.lo + _StackGuard
_g_.stackguard1 = _g_.stackguard0
// 调用mstart1来处理
mstart1() // Exit this thread.
if GOOS == "windows" || GOOS == "solaris" || GOOS == "plan9" || GOOS == "darwin" {
// Window,Solaris,Darwin和Plan 9总是系统分配堆栈,但将其放在mstart之前的_g_.stack中,因此上述逻辑尚未设置osStack。
osStack = true
}
// 退出m,正常情况下mstart1调用schedule() 时,是不再返回的,所以,不用担心系统线程的频繁创建退出
mexit(osStack)
}

mstart1

 func mstart1() {
_g_ := getg() if _g_ != _g_.m.g0 {
throw("bad runtime·mstart")
} //记录调用方,以用作mcall和用于终止线程。
     //在调用schedule之后,我们再也不会回到mstart1了,以便其他调用可以重用当前帧。 // 保存调用者的pc sp等信息
save(getcallerpc(), getcallersp())
asminit()
// 初始化m的sigal的栈和mask
minit() //安装信号处理程序; 在minit之后,minit可以准备线程以处理信号。 // 安装sigal处理器
if _g_.m == &m0 {
mstartm0()
}
// 如果设置了mstartfn,就先执行这个
if fn := _g_.m.mstartfn; fn != nil {
fn()
} if _g_.m.helpgc != {
_g_.m.helpgc =
stopm()
} else if _g_.m != &m0 {
// 获取nextp
acquirep(_g_.m.nextp.ptr())
_g_.m.nextp =
}
schedule()
}

acquirep1

 func acquirep1(_p_ *p) {
_g_ := getg() // 让m p互相绑定
_g_.m.p.set(_p_)
_p_.m.set(_g_.m)
_p_.status = _Prunning
}

schedule

  开始进入到调度函数了,这是一个由schedule、execute、goroutine fn、goexit构成的逻辑循环,就算m是唤醒后,也是从设置的断点开始执行。

  schedule函数在runtime需要进行调度时执行,为当前的P寻找一个可以运行的G并执行它,寻找顺序如下:

  • 1) 调用runqget函数来从P自己的runnable G队列中得到一个可以执行的G;
  • 2) 如果1)失败,则调用findrunnable函数去寻找一个可以执行的G;
  • 3) 如果2)也没有得到可以执行的G,那么结束调度,从上次的现场继续执行。
  • 4) 注意)//偶尔会先检查一次全局可运行队列,以确保公平性。否则,两个goroutine可以完全占用本地runqueue。 通过 schedtick计数 %61来保证
 func schedule() {
_g_ := getg() if _g_.m.locks != {
throw("schedule: holding locks")
}
// 如果有lockg,停止执行当前的m
if _g_.m.lockedg != {
// 解除lockedm的锁定,并执行当前g
stoplockedm()
execute(_g_.m.lockedg.ptr(), false) // Never returns.
} //我们不应该将正在执行cgo调用的g排开,因为cgo调用正在使用m的g0堆栈。
if _g_.m.incgo {
throw("schedule: in cgo")
} top:
// gc 等待
if sched.gcwaiting != {
gcstopm()
goto top
} var gp *g
var inheritTime bool if gp == nil {
//偶尔检查全局可运行队列以确保公平。
        //否则,两个goroutine可以完全占据本地运行队列 通过不断重生彼此。
// 为了保证公平,每隔61次,从全局队列上获取g
if _g_.m.p.ptr().schedtick% == && sched.runqsize > {
lock(&sched.lock)
gp = globrunqget(_g_.m.p.ptr(), )
unlock(&sched.lock)
}
}
if gp == nil {
// 全局队列上获取不到待运行的g,则从p local队列中获取
gp, inheritTime = runqget(_g_.m.p.ptr())
if gp != nil && _g_.m.spinning {
throw("schedule: spinning with local work")
}
}
if gp == nil {
// 如果p local获取不到待运行g,则开始查找,这个函数会从 全局 io poll, p locl和其他p local获取待运行的g,后面详细分析
gp, inheritTime = findrunnable() // blocks until work is available
} //此线程将运行goroutine并且不再旋转,因此,如果标记为正在旋转,则需要立即重置它,并可能开始一个新的旋转M。
if _g_.m.spinning {
// 如果m是自旋状态,取消自旋
resetspinning()
} if gp.lockedm != {
//将自己的p交给锁定的m,然后阻止等待新的p。
// 如果g有lockedm,则休眠上交p,休眠m,等待新的m,唤醒后从这里开始执行,跳转到top
startlockedm(gp)
goto top
}
// 开始执行这个g
execute(gp, inheritTime)
}

  因为当前的m绑定了lockedg,而当前g不是指定的lockedg,所以这个m不能执行,函数 stoplockedm() 解除lockedm的锁定,上交当前m绑定的p,并且休眠m直到调度lockedg。这个函数首先会释放当前P ->release() 然后通过 handoffp() 函数调用startm函开始调度。handoffp会判断有没有正在寻找p的m以及有没有空闲的p,如果有,尝试调用startm进行调度,如果全局队列运行g队列不为空,尝试使用startm进行调度。

  handoffp函数将P从系统调用或阻塞的M中传递出去,如果P还有runnable G队列,那么新开一个M,调用startm函数,新开的M不空旋。

  execute()函数就开始执行g的代码了:

 func execute(gp *g, inheritTime bool) {
_g_ := getg()
// 更改g的状态,并不允许抢占
casgstatus(gp, _Grunnable, _Grunning)
gp.waitsince =
gp.preempt = false
gp.stackguard0 = gp.stack.lo + _StackGuard
if !inheritTime {
// 调度计数
_g_.m.p.ptr().schedtick++
}
_g_.m.curg = gp
gp.m = _g_.m
// 开始执行g的代码了
gogo(&gp.sched)
}

  gogo函数承载的作用就是切换到g的栈,开始执行g的代码,gogo执行完函数后,是怎么再次进入调度呢?回到前面newproc1函数的L63 newg.sched.pc = funcPC(goexit) + sys.PCQuantum ,这里保存了pc的质地为goexit的地址,所以当执行完用户代码后,就会进入 goexit 函数。

goexit0

  goexit 在汇编层面就是调用 runtime.goexit1,而goexit1通过 mcall 调用了goexit0 所以这里直接分析了goexit0。goexit0 重置g的状态,并重新进行调度,这样就调度就又回到了schedule() 了,开始循环往复的调度。

 func goexit0(gp *g) {
_g_ := getg()
// 转换g的状态为dead,以放回空闲列表
casgstatus(gp, _Grunning, _Gdead)
if isSystemGoroutine(gp) {
atomic.Xadd(&sched.ngsys, -)
}
// 清空g的状态
gp.m = nil
locked := gp.lockedm !=
gp.lockedm =
_g_.m.lockedg =
gp.paniconfault = false
gp._defer = nil // 应该是真实的,但以防万一。
gp._panic = nil // 恐慌期间,对于Goexit不为零。指向堆栈分配的数据。
    gp.writebuf = nil
gp.waitreason =
gp.param = nil
gp.labels = nil
gp.timer = nil 注意,gp的堆栈扫描现在“有效”,因为它没有堆栈。
gp.gcscanvalid = true
dropg() // 把g放回空闲列表,以备复用
gfput(_g_.m.p.ptr(), gp)
// 再次进入调度循环
schedule()
}

  goexit函数是当G退出时调用的。这个函数对G进行一些设置后,将它放入free G列表中,供以后复用,之后调用schedule函数调度。

  至此,单次调度结束,再次进入调度,循环往复。

  findrunnable() 寻找一个可运行的g,过程:

  • 从p自己的local队列中获取可运行的g
  • 从全局队列中获取可运行的g
  • 从netpoll中获取一个已经准备好的g
  • 从其他p的local队列中获取可运行的g,随机偷取p的runnext,有点任性
  • 无论如何都获取不到的话,就stopm了

stopm

  stop会把当前m放到空闲列表里面,同时绑定m.nextp 与 m

 func stopm() {
_g_ := getg()
retry:
lock(&sched.lock)
// 把当前m放到sched.midle 的空闲列表里
mput(_g_.m)
unlock(&sched.lock)
// 休眠,等待被唤醒
notesleep(&_g_.m.park)
noteclear(&_g_.m.park)
// 绑定p
acquirep(_g_.m.nextp.ptr())
_g_.m.nextp =
}

1.4 监控

sysmon

  go的监控是依靠函数 sysmon 来完成的,监控主要做一下几件事:

  • 释放闲置超过5分钟的span物理内存
  • 如果超过两分钟没有执行垃圾回收,则强制执行
  • 将长时间未处理的netpoll结果添加到任务队列
  • 向长时间运行的g进行抢占
  • 收回因为syscall而长时间阻塞的p

  监控线程并不是时刻在运行的,监控线程首次休眠20us,每次执行完后,增加一倍的休眠时间,但是最多休眠10ms。

 func sysmon() {
lock(&sched.lock)
sched.nmsys++
checkdead()
unlock(&sched.lock) // 如果垃圾回收后5分钟内未使用堆范围,我们将其交还给操作系统。
scavengelimit := int64( * * 1e9) if debug.scavenge > {
// 大量测试。
forcegcperiod = * 1e6
scavengelimit = * 1e6
} lastscavenge := nanotime()
nscavenge := lasttrace := int64()
idle := // 我们没有连续唤醒多少个周期。
delay := uint32()
for {
// 判断当前循环,应该休眠的时间
if idle == { // start with 20us sleep...
delay =
} else if idle > { // start doubling the sleep after 1ms...
delay *=
}
if delay > * { // up to 10ms
delay = *
}
usleep(delay)
// STW时休眠sysmon
if debug.schedtrace <= && (sched.gcwaiting != || atomic.Load(&sched.npidle) == uint32(gomaxprocs)) {
lock(&sched.lock)
if atomic.Load(&sched.gcwaiting) != || atomic.Load(&sched.npidle) == uint32(gomaxprocs) {
atomic.Store(&sched.sysmonwait, )
unlock(&sched.lock)
// 使唤醒时间足够小,采样正确。
maxsleep := forcegcperiod /
if scavengelimit < forcegcperiod {
maxsleep = scavengelimit /
}
shouldRelax := true
if osRelaxMinNS > {
next := timeSleepUntil()
now := nanotime()
if next-now < osRelaxMinNS {
shouldRelax = false
}
}
if shouldRelax {
osRelax(true)
}
// 进行休眠
notetsleep(&sched.sysmonnote, maxsleep)
if shouldRelax {
osRelax(false)
}
lock(&sched.lock)
// 唤醒后,清除休眠状态,继续执行
atomic.Store(&sched.sysmonwait, )
noteclear(&sched.sysmonnote)
idle =
delay =
}
unlock(&sched.lock)
}
// 必要时触发libc拦截器
if *cgo_yield != nil {
asmcgocall(*cgo_yield, nil)
}
// poll network if not polled for more than 10ms
lastpoll := int64(atomic.Load64(&sched.lastpoll))
now := nanotime()
// 如果netpoll不为空,每隔10ms检查一下是否有ok的
if netpollinited() && lastpoll != && lastpoll+** < now {
atomic.Cas64(&sched.lastpoll, uint64(lastpoll), uint64(now))
// 返回了已经获取到结果的goroutine的列表
gp := netpoll(false) // non-blocking - returns list of goroutines
if gp != nil {
incidlelocked(-)
// 把获取到的g的列表加入到全局待运行队列中
injectglist(gp)
incidlelocked()
}
}
// 重新获取系统调用中阻止的P,并抢占长期运行的G
// 抢夺syscall长时间阻塞的p和长时间运行的g
if retake(now) != {
idle =
} else {
idle++
}
// check if we need to force a GC
// 通过gcTrigger.test() 函数判断是否超过设定的强制触发gc的时间间隔,
if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != {
lock(&forcegc.lock)
forcegc.idle =
forcegc.g.schedlink =
// 把gc的g加入待运行队列,等待调度运行
injectglist(forcegc.g)
unlock(&forcegc.lock)
}
// scavenge heap once in a while
// 判断是否有5分钟未使用的span,有的话,归还给系统
if lastscavenge+scavengelimit/ < now {
mheap_.scavenge(int32(nscavenge), uint64(now), uint64(scavengelimit))
lastscavenge = now
nscavenge++
}
if debug.schedtrace > && lasttrace+int64(debug.schedtrace)* <= now {
lasttrace = now
schedtrace(debug.scheddetail > )
}
}
}

  sysmon函数是Go runtime启动时创建的,负责监控所有goroutine的状态,判断是否需要GC,进行netpoll等操作。sysmon函数中会调用retake函数进行抢占式调度。跟前面添加p和m的逻辑差不多,下面看如何抢占:

retake

 const forcePreemptNS =  *  *  // 10ms

 func retake(now int64) uint32 {
n :=
//防止allp slice更改。此锁将完全,没有竞争,除非我们已经停止了世界。
lock(&allpLock)
//我们不能对allp使用范围循环,因为我们可能暂时删除allpLock。因此,我们需要重新获取每次循环时分配。
for i := ; i < len(allp); i++ {
_p_ := allp[i]
if _p_ == nil {
//如果procresize增加了,就会发生这种情况 allp,但尚未创建新的P。
continue
}
pd := &_p_.sysmontick
s := _p_.status
if s == _Psyscall {
//如果系统调用中的P超过1个sysmon滴答声(至少20us),则将其取回。
// pd.syscalltick 即 _p_.sysmontick.syscalltick 只有在sysmon的时候会更新,而 _p_.syscalltick 则会每次都更新,所以,当syscall之后,第一个sysmon检测到的时候并不会抢占,而是第二次开始才会抢占,中间间隔至少有20us,最多会有10ms
t := int64(_p_.syscalltick)
if int64(pd.syscalltick) != t {
pd.syscalltick = uint32(t)
pd.syscallwhen = now
continue
}
//一方面,如果您没有其他工作要做,我们不想重新获得P,但另一方面,我们希望最终将它们重新收录,因为它们可以防止sysmon线程进入深度睡眠状态。 // 是否有空p,有寻找p的m,以及当前的p在syscall之后,有没有超过10ms
if runqempty(_p_) && atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > && pd.syscallwhen+** > now {
continue
}
//删除allpLock,以便我们使用sched.lock。
            //解锁(&allpLock),需要减少空闲锁定M的数量
            //(假设还有一个正在运行)。
            //否则,我们从中夺回的M可以退出系统调用,递增nmidle并报告deadlock.sleep。
incidlelocked(-)
// 抢占p,把p的状态转为idle状态
if atomic.Cas(&_p_.status, s, _Pidle) {
if trace.enabled {
traceGoSysBlock(_p_)
traceProcStop(_p_)
}
n++
_p_.syscalltick++
// 把当前p移交出去,上面已经分析过了
handoffp(_p_)
}
incidlelocked()
lock(&allpLock)
} else if s == _Prunning {
//如果G运行时间过长,则抢占G。
// 如果p是running状态,如果p下面的g执行太久了,则抢占
t := int64(_p_.schedtick)
if int64(pd.schedtick) != t {
pd.schedtick = uint32(t)
pd.schedwhen = now
continue
}
// 判断是否超出10ms, 不超过不抢占
if pd.schedwhen+forcePreemptNS > now {
continue
}
// 开始抢占
preemptone(_p_)
}
}
unlock(&allpLock)
return uint32(n)
}

preemptone

  抢占实现:

 func preemptone(_p_ *p) bool {
mp := _p_.m.ptr()
if mp == nil || mp == getg().m {
return false
}
gp := mp.curg
if gp == nil || gp == mp.g0 {
return false
}
// 标识抢占字段
gp.preempt = true // go例程中的每个调用都检查堆栈溢出,比较当前堆栈指针与gp-> stackguard0。
    //将gp-> stackguard0设置为StackPreempt折叠,抢占正常的堆栈溢出检查。 // 更新stackguard0,保证能检测到栈溢
gp.stackguard0 = stackPreempt
return true
}

  在这里,作者会更新  gp.stackguard0 = stackPreempt,然后让g误以为栈不够用了,那就只有乖乖的去进行栈扩张,站扩张的话就用调用newstack 分配一个新栈,然后把原先的栈的内容拷贝过去,而在 newstack 里面有一段如下。

 if preempt {
if thisg.m.locks != || thisg.m.mallocing != || thisg.m.preemptoff != "" || thisg.m.p.ptr().status != _Prunning {
//让goroutine现在继续运行。
        //已设置gp-> preempt,因此下一次将被抢占。
gp.stackguard0 = gp.stack.lo + _StackGuard
gogo(&gp.sched) // never return
}
}

  然后这里就发现g被抢占了,这种抢占方式自动1.5(也可能更早)就一直存在,且稳定运行。

2. 总结

  调度器

  • 复用线程:协程本身就是运行在一组线程之上,不需要频繁的创建、销毁线程,而是对线程的复用。在调度器中复用线程还有2个体现:1)work stealing,当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程。2)handoff,当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。
  • 利用并行:GOMAXPROCS设置P的数量,当GOMAXPROCS大于1时,就最多有GOMAXPROCS个线程处于运行状态,这些线程可能分布在多个CPU核上同时运行,使得并发利用并行。另外,GOMAXPROCS也限制了并发的程度,比如GOMAXPROCS = 核数/2,则最多利用了一半的CPU核进行并行。
  • 抢占:在coroutine中要等待一个协程主动让出CPU才执行下一个协程,在Go中,一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死,这就是goroutine不同于coroutine的一个地方。
  • 全局G队列:在新的调度器中依然有全局G队列,但功能已经被弱化了,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G。

  多级缓存: 这一块跟内存上的设计思想也是一直的,p一直有一个 g 的待运行队列,自己没有货过多的时候,才会平衡到全局队列,全局队列操作需要锁,则本地操作则不需要,大大减少了锁的创建销毁所消耗的资源

go中的关键字-go(下)的更多相关文章

  1. Java中的关键字 transient

    先解释下Java中的对象序列化 在讨论transient之前,有必要先搞清楚Java中序列化的含义: Java中对象的序列化指的是将对象转换成以字节序列的形式来表示,这些字节序列包含了对象的数据和信息 ...

  2. js中this关键字测试集锦

    参考:阮一峰<javascript的this用法>及<JS中this关键字详解> this是Javascript语言的一个关键字它代表函数运行时,自动生成的一个内部对象,只能在 ...

  3. 【转载】C/C++中extern关键字详解

    1 基本解释:extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义.此外extern也可用来进行链接指定. 也就是说extern ...

  4. 【转】java中volatile关键字的含义

    java中volatile关键字的含义   在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言 ...

  5. C/C++中extern关键字解析

    1 基本解释:extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义.此外extern也可用来进行链接指定. 也就是说extern ...

  6. Java中native关键字

    Java中native关键字 标签: Java 2016-08-17 11:44 54551人阅读 评论(0) 顶(23453) 收藏(33546)   今日在hibernate源代码中遇到了nati ...

  7. C/C++中static关键字详解

    静态变量作用范围在一个文件内,程序开始时分配空间,结束时释放空间,默认初始化为0,使用时可以改变其值. 静态变量或静态函数只有本文件内的代码才能访问它,它的名字在其它文件中不可见.用法1:函数内部声明 ...

  8. C/C++中extern关键字详解

    转自:http://www.cnblogs.com/yc_sunniwell/archive/2010/07/14/1777431.html 1 基本解释:extern可以置于变量或者函数前,以标示变 ...

  9. 【转载】理解C语言中的关键字extern

    原文:理解C语言中的关键字extern 最近写了一段C程序,编译时出现变量重复定义的错误,自己查看没发现错误.使用Google发现,自己对extern理解不透彻,我搜到了这篇文章,写得不错.我拙劣的翻 ...

随机推荐

  1. 告别10kb/s的Github访问速度

    由于种种原因,国内访问Github的体验一直不是很好.本文通过优化DNS缓存的方式,避免浏览器直接解析Github域名,来改善Github的访问速度. 本文分为如下三个部分: 通过IP地址查询获取访问 ...

  2. 使用 pdf.js 跨域问题的处理方法1

    在<使用 pdf.js 在网页中加载 pdf 文件>中详细介绍了 pdf.js 的使用与集成网页开发的基本方法.展示效果如下图: 站点的目录为 http://localhost:8033/ ...

  3. 设计模式(十八)Memento模式

    在使用面向对象编程的方式实现撤销功能时,需要事先保存实例的相关状态信息.然后,在撤销时,还需要根据所保存的信息将实例恢复至原来的状态. 要想恢复实例,需要一个可以自由访问实例内部结构的权限.但是,如果 ...

  4. sqlite复制表

    (1)复制表,并把原表的 所有记录都复制到新表里. CREATE TABLE newTb AS SELECT * FROM oldTb (2)只复制表结构,不复制数据到新表里. 注:该语句无法复制关键 ...

  5. C# 8 - 其它新特性

    其它关于C# 8和.NET Core 3.0新特性的文章: C# 8 - Nullable Reference Types 可空引用类型 C# 8 - 模式匹配 C# 8 - Range 和 Inde ...

  6. .net调用阿里短信接口

    一.创建一个空的api项目 二.应用阿里的短信包 aliyun-net-sdk-core 三.登录阿里添加签名和模板 四.创建创建AccessKey 注意 AccessKey创建后,无法再通过控制台查 ...

  7. Mybaits 源码解析 (八)----- 全网最详细,没有之一:结果集 ResultSet 自动映射成实体类对象(上篇)

    上一篇文章我们已经将SQL发送到了数据库,并返回了ResultSet,接下来就是将结果集 ResultSet 自动映射成实体类对象.这样使用者就无需再手动操作结果集,并将数据填充到实体类对象中.这可大 ...

  8. MIT线性代数:22.对角化和A的幂

  9. MIT线性代数:16.投影矩阵和最小二乘

  10. 如何将excel文件导入testlink

    Step 1 按照excel模板设计测试用例,其中优先级的定义为: 数值 定义 1 LOW 2 MEDIUM 3 HIGH Step 2 执行脚本,将excel转换成xml: 脚本 备注 包含:exc ...