资源链接
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)
)的情况:
- 执行完成当前g,在调度过程中,窃取不到其他的g,则会被加入空闲队列(
schedule()
函数中查找本地队列无可用g,调用findrunnable()
函数,仍找不到g,则调用pidleput(p)
)
findrunnable()
:本地队列获取g→全局队列获取g→从netpoll获取→其他p处偷g(一般从队列偷一半,如果偷不到,则尝试偷从其他p的 p.runnext 偷取)- G的窃取规则:在偷取 g 的时候,从 allp 中通过伪随机方式访问 p,保证公平性
- m退出,m和p解绑,并将p加入空闲队列(
handoff(release(p))
函数中调用pidleput(p)
) - 修改了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)
问题
看源码的时候,有可能出现只有声明但是没有函数体的函数情况,大致以下三种:
- 函数体是汇编代码写的
- 利用编译指示,来获取真正的函数body,link的本质是把函数的名字link到当前的声明里面
比如函数上面写了go:xxx
,xxx
为 nickname - 由编译器帮忙重写
汇编代码和代码里面都是看不到实现方式的,相当于代码逻辑都在编译器里面
比如runtime.getg()