【Go夜读】goroutine 的调度

资源链接

github资源
B站视频资源
Go协程(goroutine)与Python协程(yield)

深入golang runtime的调度


调度的机制用一句话描述:
runtime准备好M, P, G,然后M绑定一个P,最开始创建g0,然后调度g0,通过g0创建G,M从各种队列中获取G,在汇编代码层面上切换到G的执行栈上并执行G上的任务函数,执行完成后,进入 goexit()(goexit 函数的地址在事前会放入G的pc计数器中,所以协程执行最后return后会直接进入 goexit 函数)做清理工作并回到M,M重新在队列中寻找G,如此反复。
运行函数 schedule() --找G--> execute(g) --执行G,gogo(g)在汇编代码层面上真正执行G--> goexit() -- 清理工作,重新将g0加入P的空闲队列--> schedule()
可以发现上面是 schedule execute goexit 三个函数的嵌套调用,但是并不会导致栈爆炸,因为每次执行 schedule,都是从栈的同一个地方开始,goexit会清理掉之前 g 留在栈上面的信息。

调度循环的建立

基本概念


M(machine)

  • M代表着真正的执行计算资源,可以认为它就是os thread(系统线程),是线程的本地私有变量。每个M绑定一个 kernel。
  • M是真正调度系统的执行者,总是从各种队列(全局队列,本局队列等)中找到可运行的G,而且这样M的可以同时存在多个。每个M都有一个 g0 对象,负责提供运行栈来运行 schedule 函数;还有一个 curg 对象,表示现在正在运行的 g 对象。
  • M在绑定有效的P后,进入调度循环,而且M并不保留G状态,这是G可以跨M调度的基础。
  • M个数没有限制,永远保持一个M处于自旋状态(寻找工作中),如果没有就会新创建一个M。
  • M被创建后不会被删除,所占用的资源不会释放,即使永远不被使用,不被使用的M处于休眠状态等待被唤醒。
  • 所有的M都被放在 allm 全局变量中。

P(processor)

  • P表示逻辑processor,是线程M的执行的上下文。
  • P的最大作用是其拥有的各种G对象队列、链表、cache和状态。
  • P的个数是 GOENV.GOMAXPROCS,可以调整,但是调整后会 STW。在以前的版本中,它默认为1。从Go 1.5开始,默认值为核心数。可以显式设置。
  • P有一个本地队列存放G,大小256,优先执行本地队列的G,当没有G,则去全局队列找G,还没有则去其他P中窃取G。
  • 所有的P都被放在 allp 全局变量中。

G(goroutine)

  • 调度系统的最基本单位goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等。G拥有自己的运行栈,有自己的PC(指令地址)、SP(栈顶)和BP(栈底)等,将这些值传入CPU寄存器,即可开始运行。
  • 在G的眼中只有P,P就是运行G的“CPU”。
  • 相当于两级线程。
  • G创建后,先放入P的本地G队列(大小256),当本地队列满了,则放入全局队列。
  • 当某个G持续执行时长过长(10ms),那么这个G会被挂起放入队尾,同时这个P执行时长也会被认定为过长,该P与M解绑,重新去绑定一个M
  • 所有的G都被放在 allgs 全局变量中。

M 的状态很少,G最多。一开始的Go是只有M和G的,但是存在很多的全局锁,导致性能很慢,后来加了P,有了本地队列,减少了锁。一个P底下最多有256个G(本地队列的长度),多余的G会被放入全局队列中。

线程实现模型

来自Go并发编程实战
                    +-------+       +-------+      
                    |  KSE  |       |  KSE  |          
                    +-------+       +-------+      
                        |               |                       内核空间
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -        
                        |               |                       用户空间
                    +-------+       +-------+
                    |   M   |       |   M   |
                    +-------+       +-------+
                  |          |         |          |
              +------+   +------+   +------+   +------+            
              |   P  |   |   P  |   |   P  |   |   P  |
              +------+   +------+   +------+   +------+   
           |     |     |     |     |     |     |     |     | 
         +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ 
         | G | | G | | G | | G | | G | | G | | G | | G | | G | 
         +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ 

KSE(Kernel Scheduling Entity)是内核调度实体
M与P,P与G之前的关联都是动态的,可以变的

关系示意图

来自golang源码剖析
                            +-------------------- sysmon ---------------//------+ 
                            |                                                   |
                            |                                                   |
               +---+      +---+-------+                   +--------+          +---+---+
go func() ---> | G | ---> | P | local | <=== balance ===> | global | <--//--- | P | M |
               +---+      +---+-------+                   +--------+          +---+---+
                            |                                 |                 | 
                            |      +---+                      |                 |
                            +----> | M | <--- findrunnable ---+--- steal <--//--+
                                   +---+ 
                                     |
                                   mstart
                                     |
              +--- execute <----- schedule 
              |                      |   
              |                      |
              +--> G.fn --> goexit --+ 

-----------解释:
              1. go func() 语句创建G。
              2. 将G放入P的本地队列(或者平衡到全局全局队列)。
              3. 唤醒或新建M来执行任务。
              4. 进入调度循环
              5. 尽力获取可执行的G,并执行
              6. 清理现场并且重新进入调度循环

上图的 schedule 循环,是调度循环,是不会停止的,通过环内的函数不断进行互相调用,而一直执行下去。

必须了解的思想


Worker thread parking/unparking

  • 涉及到 m 的 spinning 和 unspinning 状态
  • 涉及到 gorotine ready 时候的操作
    我们需要在`保持足够的 running(运行中) 工作线程以利用可用的硬件并行性` 与`停放过多的运行中工作线程以节省CPU资源和功耗`之间进行权衡。
    这并不简单,原因有二:(1)调度状态(scheduler state)是有意分配的(特别是针对每个P的工作队列),因此无法在 fast paths 上计算全局预测; 
                        (2)为了实现最佳的线程管理,我们需要知道未来的情况(当一个 goroutine 不久将被 readied 时,则不需要停靠工作线程)。

    三种被弃用的效果很差的方法:
        1.集中所有调度状态(scheduler state)
            将抑制可伸缩性)。
        2.直接切换goroutine。 
            也就是说,当我们准备好一个新的goroutine并且当前有一个备用P时,释放一个线程,并将该线程和goroutine转交给P。
            这将导致线程状态冲突(thread state thrashing),因为准备好goroutine的线程可能下一刻就停止了工作,我们需要 park 该线程。
            另外,由于我们要在同一线程上保留依赖的goroutine,它将破坏计算的局部性。 并引入额外的延迟。
        3.每当我们准备好goroutine并且有一个空闲的P时,unpark 一个附加线程,但不进行切换。 
            这将导致过多的线程 parking/unparking,因为附加线程没有发现要执行的工作将立即 park。

    当前方法:准备好一个goroutine时,如果当前有一个空闲 P 且没有“spinning”工作线程(即处于 spinning 状态的 M),则我们 unpark 一个附加线程。
            (“spinning”指一个工作线程 M 完成了本地工作,并且在全局 run queue / netpoller 中均未找到工作)
             the spinning state 用 m.spinning 和 sched.nmspinning 来表示,前者表示 M 是否在 spinning状态,后者表示在 spinning 状态的 M 个数。

             通过上述方式 unpark 的线程也被认为是 spinning 状态的。此时我们不执行goroutine切换,因此此类线程最初是没有工作的。
             “spinning”线程在 park 之前,会在 P 的运行队列中寻找工作。如果 spinning 线程找到工作,它将退出 spinning state 并继续执行工作。如果找不到工作,它将退出 spinning state,然后 park。
    如果至少有一个“spinning”线程(sched.nmspinning> 1),则在准备goroutine时我们不会 unpark 新线程。为了弥补这一点,如果最后一个“spinning”线程找到了工作并停止“spinning”,则必须 unpark 一个新的“spinning”线程。
    这种方法可以消除线程 unparking 中的不合理的峰值,但同时可以保证最终的最大CPU并行利用率。

    实现的主要复杂之处在于,我们在线程从 spinning->non-spinning 过渡时,需要非常小心。 这种过渡可能会与新goroutine的提交相互竞争,同时一部分或另一部分需要 unpark 另一个工作线程。如果它们俩都失败了,那么我们可能会导致半永久性的CPU利用率不足。
    goroutine准备的一般模式是:将goroutine提交到本地工作队列,#StoreLoad-style memory barrier,检查sched.nmspinning。
    spinning->non-spinning 过渡的一般模式是:递减nmspinning,#StoreLoad-style memory barrier,检查所有 P 的本地工作队列中是否有新工作。
    请注意,所有这些复杂性都不适用于全局运行队列,因为在提交到全局队列时,我们对线程的 unparking 并不草率。 另请参见有关nmspinning操作的注释。

GPM的来由


g0和m0是在proc.go文件中的两个全局变量
m0:进程启动后的初始线程
g0:代表着初始线程的stack
asm_amd64.go --> runtime·rt0_go(SB)

    // 程序刚启动的时候必定有一个线程启动(主线程)
    // 将当前的栈和资源保存在g0
    // 将该线程保存在m0
    // tls: Thread Local Storage
    // set the per-goroutine and per-mach "registers"
    get_tls(BX)
    LEAQ    runtime·g0(SB), CX
    MOVQ    CX, g(BX)
    LEAQ    runtime·m0(SB), AX

    // save m->g0 = g0
    MOVQ    CX, m_g0(AX)
    // save m0 to g0->m
    MOVQ    AX, g_m(CX)

M的一生

M的创建

proc.go

// Create a new m. It will start off with a call to fn, or else the scheduler.
// fn needs to be static and not a heap allocated closure.
// May run with m.p==nil, so write barriers are not allowed.
//go:nowritebarrierrec
// 创建一个新的m,它将从fn或者调度程序开始
func newm(fn func(), _p_ *p) {
    // 根据fn和p和绑定一个m对象
    mp := allocm(_p_, fn)
    // 设置当前m的下一个p为_p_
    mp.nextp.set(_p_)
    mp.sigmask = initSigmask
    ...
    // 真正的分配os thread
    newm1(mp)
}
func newm1(mp *m) {
    // 对cgo的处理
    ...
    execLock.rlock() // Prevent process clone.
    // 创建一个系统线程
    newosproc(mp, unsafe.Pointer(mp.g0.stack.hi))
    execLock.runlock()
}

M的状态

sched.nmspinning 保存 spinning 的 m 个数

m.spinning value 含义
spinning true m is out of work and is actively looking for work
unspinning false m is working
       mstart
          |
          |            找不到可执行任务;
          |            gc STW;
          v            任务执行时间过长;
      +----------+     系统阻塞等                   +----------+
      | spinning | ------------------------------> |unspinning| 
      +----------+            mstop                +----------+
          ^                                          ^
          |                                          |
      notewakeup <-----------------------------  notesleep

M的问题

M的问题
线程不会被释放,即便不用

P的一生

P的创建

proc.go

// Change number of processors. The world is stopped, sched is locked.
// gcworkbufs are not being modified by either the GC or
// the write barrier code.
// Returns list of Ps with local work, they need to be scheduled by the caller.
// 所有的P都在这个函数分配,不管是最开始的初始化分配,还是后期调整
func procresize(nprocs int32) *p {
    // 默认传入的 nprocs 就是 CPU 个数,不能为 0

    old := gomaxprocs
    // 如果 gomaxprocs <=0 抛出异常
    if old < 0 || nprocs <= 0 {
        throw("procresize: invalid arg")
    }
  ...
    // Grow allp if necessary. allp 是全局数组
    if nprocs > int32(len(allp)) {
        // Synchronize with retake, which could be running
        // concurrently since it doesn't run on a P.
        lock(&allpLock)
        if nprocs <= int32(cap(allp)) {
            allp = allp[:nprocs]
        } else {
            // 分配nprocs个*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
    for i := int32(0); i < nprocs; i++ {
        pp := allp[i]
        if pp == nil {
            pp = new(p)
            pp.id = i
            pp.status = _Pgcstop            // 更改状态
            pp.sudogcache = pp.sudogbuf[:0] //将sudogcache指向sudogbuf的起始地址
            for i := range pp.deferpool {
                pp.deferpool[i] = pp.deferpoolbuf[i][:0]
            }
            pp.wbBuf.reset()
            // 将pp保存到allp数组里, 下面这行代码等价于 allp[i] = pp
            atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
        }
        ...
    }
  ...

    _g_ := getg()
    // 如果当前的M已经绑定P,继续使用,否则将当前的M绑定一个P
    if _g_.m.p != 0 && _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]
        // 获取allp[0]
        if _g_.m.p != 0 {
            _g_.m.p.ptr().m = 0
        }
        _g_.m.p = 0
        _g_.m.mcache = nil
        p := allp[0]
        p.m = 0
        p.status = _Pidle
        // 将当前的m和p绑定
        acquirep(p)
        if trace.enabled {
            traceGoStart()
        }
    }
    var runnablePs *p
    for i := nprocs - 1; i >= 0; i-- {
        p := allp[i]
        if _g_.m.p.ptr() == p {
            continue
        }
        p.status = _Pidle
        // 判断当前的 p 是不是被绑定
        if runqempty(p) { // 将空闲p放入空闲链表
            pidleput(p)
        } else {
            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
}

所有的P在程序启动的时候就设置好了,并用一个allp slice维护,可以调用runtime.GOMAXPROCS调整P的个数,虽然代价很大(会停止世界 stopTheWorld,里面会 stop go)

P的状态

runtime2.go中的全局变量 allp 存储所有可以拿到的 p
sched.pidle 存储所有的空闲的 P,是 P 的空闲队列(链表,sched.pidle存储一个p指针,p.link存储下一个p指针)

P被放入空闲队列(pidleput(p))的情况:

  1. 执行完成当前g,在调度过程中,窃取不到其他的g,则会被加入空闲队列(schedule()函数中查找本地队列无可用g,调用findrunnable()函数,仍找不到g,则调用 pidleput(p)
    findrunnable():本地队列获取g→全局队列获取g→从netpoll获取→其他p处偷g(一般从队列偷一半,如果偷不到,则尝试偷从其他p的 p.runnext 偷取)
    • G的窃取规则:在偷取 g 的时候,从 allp 中通过伪随机方式访问 p,保证公平性
  2. m退出,m和p解绑,并将p加入空闲队列(handoff(release(p))函数中调用 pidleput(p)
  3. 修改了GOMAXPROCS后,世界停止,调度停止。(procresize(nprocs)函数中调用 pidleput(p)
    如果GOMAXPROCS减小,则多余的p进入 _Pdead;
    如果GOMAXPROCS增大,则创建缺少的 p;
    对于所有即将使用的 p (修改后的GOMAXPROCS个p),本地队列没有 go 任务的 p ,加入空闲队列

runtime2.go

p.status value 执行用户代码 位于空闲队列 分配了M 含义
_Pidle 0 × P没有被用来执行用户代码,也没有被调度,而是被放在 the idle P list,可以被调度器获取。也可能只是在状态转换的中间过程中
_Prunning 1 × P被一个M所拥有,用来运行用户代码或者调度器,只有拥有该P的M能够从这个状态修改为其他状态(比如转化为:_Pidle-没有工作需要做;_Psyscall-进入一个系统调用;_Pgcstop-停下来执行gc)。M也可以将P交给其他的M(比如调度一个加锁的G)
_Psyscall 2 × × P没有运行用户代码。 它与系统调用中的M绑定,但不属于它,并且可能被另一个M窃取。这类似于_Pidle,但使用轻量级的状态转换,同时与M绑定。离开_Psyscall 必须与CAS(atomic.Cas 原子操作的状态转换函数)一起调用,以窃取或重新获得P。请注意,这存在ABA危险:即使M在syscall后成功将其原始P返回_Prunning状态,它也必须了解P可能在此期间已被另一个M使用 。
_Pgcstop 3 × × 执行 stopTheWorld()(位于proc.go,用于暂停所有的G,简称STW) 时暂停P,由 STW的M拥有。STW的M甚至在_Pgcstop中也继续使用其P。 从_Prunning过渡到_Pgcstop会导致M释放其P并停放。P保留其运行队列,startTheWorld将在具有非空运行队列的Ps上重新启动调度程序 schedule()。
_Pdead 4 × × × P不再被使用,而被放入空闲队列(GOMAXPROCS缩小)。如果GOMAXPROCS增加,我们将重用P。一个死掉的P被剥夺了其大部分资源,尽管还剩下一些东西(例如,跟踪缓冲区)。
                                             acquirep(p)        
                          不需要使用的P       P和M绑定的时候       进入系统调用       procresize()
new(p)  -----+        +---------------+     +-----------+     +------------+    +----------+
            |         |               |     |           |     |            |    |          |
            |   +------------+    +-- v -------+    +-- v -------+    +--- v ------+    +- v --------+
            +-->|  _Pgcstop  |    |    _Pidle  |    |  _Prunning |    |  _Psyscall |    |   _Pdead   |
                +----- ^ ----+    +------ ^ ---+    +------- ^ --+    +------------+    +------------+
                       |            |     |            |     |            |
                       +------------+     +------------+     +------------+
                           GC结束            releasep()        退出系统调用
                                             P和M解绑                      

P的数量默认等于cpu的个数,很多人认为runtime.GOMAXPROCS可以限制系统线程的数量,但这是错误的,M是按需创建的,和runtime.GOMAXPROCS没有关系。
如果一开始runtime.GOMAXPROCS=10,之后修改成5,那么有5个P不允许使用,那么这些P进入_Pdead 状态。如果再次调整runtime.GOMAXPROCS=10,就会改状态为 _Pgcstop

G的一生

G的创建

proc.go

// Create a new g running fn with siz bytes of arguments.
// Put it on the queue of g's waiting to run.
// The compiler turns a go statement into a call to this.
// Cannot split the stack because it assumes that the arguments
// are available sequentially after &fn; they would not be
// copied if a stack split occurred.
//go:nosplit
// 新建一个goroutine,
// 用fn + PtrSize 获取第一个参数的地址,也就是argp
// 用siz - 8 获取pc地址
func newproc(siz int32, fn *funcval) {
    argp := add(unsafe.Pointer(&fn), sys.PtrSize)
    pc := getcallerpc()
    // 用g0的栈创建G对象
    systemstack(func() {
        // 真正创建
        newproc1(fn, (*uint8)(argp), siz, pc)
    })
}
// Create a new g running fn with narg bytes of arguments starting
// at argp. callerpc is the address of the go statement that created
// this. The new g is put on the queue of g's waiting to run.
// 根据函数参数和函数地址,创建一个新的G,然后将这个G加入队列等待运行
func newproc1(fn *funcval, argp *uint8, narg int32, callerpc uintptr) {
    // 获取当前g
    _g_ := getg()

    if fn == nil {
        _g_.m.throwing = -1 // do not dump full stacks
        throw("go of nil func value")
    }
    _g_.m.locks++ // disable preemption because it can be holding p in a local var
    siz := narg
    siz = (siz + 7) &^ 7

    // 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).
    // 如果函数的参数大小比2048大的话,直接panic
    // 这里的sys.RegSize是根据系统会有区别的,比如64位就是8字节,32位就是4字节
    if siz >= _StackMin-4*sys.RegSize-sys.RegSize {
        throw("newproc: function arguments too large for new goroutine")
    }

    // 从当前g的m中获取p
    _p_ := _g_.m.p.ptr()
    // 从gfree list获取g
    newg := gfget(_p_)
    // 如果没获取到g,则新建一个
    if newg == nil {
        newg = malg(_StackMin)
        casgstatus(newg, _Gidle, _Gdead) //将g的状态改为_Gdead
        // 添加到allg数组,防止gc扫描清除掉
        allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
    }
    if newg.stack.hi == 0 {
        throw("newproc1: newg missing stack")
    }

    if readgstatus(newg) != _Gdead {
        throw("newproc1: new g is not Gdead")
    }

    totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frame
    totalSize += -totalSize & (sys.SpAlign - 1)                  // align to spAlign
    sp := newg.stack.hi - totalSize
    spArg := sp
    if usesLR {
        // caller's LR
        *(*uintptr)(unsafe.Pointer(sp)) = 0
        prepGoExitFrame(sp)
        spArg += sys.MinFrameSize
    }
    if narg > 0 {
        // copy参数
        memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg))
        // This is a stack-to-stack copy. If write barriers
        // are enabled and the source stack is grey (the
        // destination is always black), then perform a
        // barrier copy. We do this *after* the memmove
        // because the destination stack may have garbage on
        // it.
        if writeBarrier.needed && !_g_.m.curg.gcscandone {
            f := findfunc(fn.fn)
            stkmap := (*stackmap)(funcdata(f, _FUNCDATA_ArgsPointerMaps))
            // We're in the prologue, so it's always stack map index 0.
            bv := stackmapdata(stkmap, 0)
            bulkBarrierBitmap(spArg, spArg, uintptr(narg), 0, bv.bytedata)
        }
    }

    memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
    // 下面是对新创建好的g设置各种参数,之后的调度就是根据参数走的
    newg.sched.sp = sp
    newg.stktopsp = sp
    // 保存goexit的地址到sched.pc
    newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
    newg.sched.g = guintptr(unsafe.Pointer(newg))
    gostartcallfn(&newg.sched, fn)
    newg.gopc = callerpc
    newg.startpc = fn.fn
    if _g_.m.curg != nil {
        newg.labels = _g_.m.curg.labels
    }
    if isSystemGoroutine(newg) {
        atomic.Xadd(&sched.ngsys, +1)
    }
    newg.gcscanvalid = false
    // 更改当前g的状态为_Grunnable
    casgstatus(newg, _Gdead, _Grunnable)

    // 生成唯一的goid
    if _p_.goidcache == _p_.goidcacheend {
        // Sched.goidgen is the last allocated id,
        // this batch must be [sched.goidgen+1, sched.goidgen+GoidCacheBatch].
        // At startup sched.goidgen=0, so main goroutine receives goid=1.
        _p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch)
        _p_.goidcache -= _GoidCacheBatch - 1
        _p_.goidcacheend = _p_.goidcache + _GoidCacheBatch
    }
    // 分配给g
    newg.goid = int64(_p_.goidcache)
    _p_.goidcache++
    if raceenabled {
        newg.racectx = racegostart(callerpc)
    }
    if trace.enabled {
        traceGoCreate(newg, newg.startpc)
    }
    // 将当前新生成的g,放入p的队列;p是从当前的g的m中获取的;如果队列没满就放在本地队列,否则会放入全局队列
    runqput(_p_, newg, true)

    // 如果有空闲的p 且 m没有处于自旋状态 且 main goroutine已经启动,那么唤醒某个m来执行任务
    if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {
        wakep()
    }
    _g_.m.locks--
    if _g_.m.locks == 0 && _g_.preempt { // restore the preemption request in case we've cleared it in newstack
        _g_.stackguard0 = stackPreempt
    }
} 

G的状态

runtime2.go

g.atomicstatus value 执行用户代码 位于运行队列 拥有栈 分配了M 分配了 P 含义
_Gidle 0 × × × × × 分配了空间,但是并未被初始化
_Grunnable 1 × × goroutine在运行队列(run queue),但是并没有执行用户代码,没有享有栈
_Grunning 2 × goroutine可能在执行用户代码(或者做一些其他操作),其拥有栈,不在运行队列,被分配了一个M和一个P
_Gsyscall 3 × × goroutine正在执行一个系统调用,并没有在执行用户代码,拥有栈,不在运行队列,被分配了一个M
_Gwaiting 4 × × × 在运行时被阻塞,并没有在执行用户代码,不在运行队列,但是会在某个地方被记录(比如 channel、wait queue),所以在条件允许后会调用 ready() 进入 _Grunnable 状态,并放在运行队列。
通道操作可以在适当的通道锁下读取或写入堆栈的某些部分外,不拥有该堆栈。故在goroutine输入_Gwaiting之后访问堆栈是不安全的
_Gmoribund_unused 5 当前未使用,但在gdb脚本中进行了硬编码
_Gdead 6 × × × × goroutine当前未被使用,可能直接退出了,在空闲列表中,或者是仅仅被初始化完成,没有执行用户代码,可能拥有或者不拥有堆栈,G和其堆栈被M所拥有,当前的M正在exiting G或者将G从空闲队列拿出来
_Genqueue_unused 7 当前未被使用
_Gcopystack 8 × × × goroutine的堆栈正在被移动。未在执行用户代码,不在运行队列,堆栈被将其置为 _Gcopystack 状态的goroutine所拥有
_Gpreempted 9 × × × goroutine停下来从而被一个suspendG抢占,类似 _Gwaiting 状态,但是没有地方负责 ready() 它。一些 suspendG 必须改变状态至 _Gwaiting 来负责调用本G的 ready()
                               --------------------------------------------------------
                               |                      +------------+                  |
                               |      ready           |            |                  |
                               |  +------------------ |  _Gwaiting |                  |
                               |  |                   |            |                  | newproc
                               |  |                   +------------+                  |
                               |  |                         ^ park_m                  |
                               V  V                         |                         |
  +------------+            +------------+  execute   +------------+            +------------+    
  |            |  newproc   |            | ---------> |            |   goexit   |            |
  |  _Gidle    | ---------> | _Grunnable |  yield     | _Grunning  | ---------> |   _Gdead   |      
  |            |            |            | <--------- |            |            |            |
  +------------+            +------------+            +------------+            +------------+
                                  ^         entersyscall |      ^ 
                                  |                      V      | existsyscall
                                  |                   +------------+
                                  |   existsyscall    |            |
                                  +------------------ |  _Gsyscall |
                                                      |            |
                                                      +------------+

最开始是初始化值:0,就是 _Gidle 状态
新建的G都是_Grunnable的,新建G的时候优先从gfree list从获取G,这样可以复用G,所以上图的状态不是完整的,_Gdead通过newproc会变为_Grunnable, 通过go func()的语法新建的G,并不是直接运行,而是放入可运行的队列中,并不能决定其什么时候运行,而是靠调度系统去自发的运行。
_Gdead 也可能直接变为 _Grunnable,比如上面的代码从gfree list获取g newg := gfget(_p_)的时候,可能拿到的就是 _Gdead 状态的g,之后更改当前g的状态为_Grunnable casgstatus(newg, _Gdead, _Grunnable)

问题


看源码的时候,有可能出现只有声明但是没有函数体的函数情况,大致以下三种:

  1. 函数体是汇编代码写的
  2. 利用编译指示,来获取真正的函数body,link的本质是把函数的名字link到当前的声明里面
    比如函数上面写了 go:xxxxxx为 nickname
  3. 由编译器帮忙重写
    汇编代码和代码里面都是看不到实现方式的,相当于代码逻辑都在编译器里面
    比如 runtime.getg()

你可能感兴趣的:(【Go夜读】goroutine 的调度)