golang内幕之协程状态切换 - Go语言中文社区

golang内幕之协程状态切换


 

本文承接上一篇文章【golang内幕之程序启动流程】【https://blog.csdn.net/QQ1130141391/article/details/96197570

在 【golang内幕之程序启动流程】文中我们提到了在主线程中启动了main Goroutine,并提到了schedule是一轮的协程调度,并且是永不返回的,这正是我们研究的入口点;另外也提到了go func(){}语句创建一个协程,实际上对应的是newproc函数,这是我们研究的另外一个入口点。

现在我们先看Goroutine的状态都有哪些状态,Goroutine状态声明在runtime/runtime2.go文件中:

const (
	// G status
	//
	// Beyond indicating the general state of a G, the G status
	// acts like a lock on the goroutine's stack (and hence its
	// ability to execute user code).
	//
	// If you add to this list, add to the list
	// of "okay during garbage collection" status
	// in mgcmark.go too.

	// _Gidle means this goroutine was just allocated and has not
	// yet been initialized.
	_Gidle = iota // 0

	// _Grunnable means this goroutine is on a run queue. It is
	// not currently executing user code. The stack is not owned.
	_Grunnable // 1

	// _Grunning means this goroutine may execute user code. The
	// stack is owned by this goroutine. It is not on a run queue.
	// It is assigned an M and a P.
	_Grunning // 2

	// _Gsyscall means this goroutine is executing a system call.
	// It is not executing user code. The stack is owned by this
	// goroutine. It is not on a run queue. It is assigned an M.
	_Gsyscall // 3

	// _Gwaiting means this goroutine is blocked in the runtime.
	// It is not executing user code. It is not on a run queue,
	// but should be recorded somewhere (e.g., a channel wait
	// queue) so it can be ready()d when necessary. The stack is
	// not owned *except* that a channel operation may read or
	// write parts of the stack under the appropriate channel
	// lock. Otherwise, it is not safe to access the stack after a
	// goroutine enters _Gwaiting (e.g., it may get moved).
	_Gwaiting // 4

	// _Gmoribund_unused is currently unused, but hardcoded in gdb
	// scripts.
	_Gmoribund_unused // 5

	// _Gdead means this goroutine is currently unused. It may be
	// just exited, on a free list, or just being initialized. It
	// is not executing user code. It may or may not have a stack
	// allocated. The G and its stack (if any) are owned by the M
	// that is exiting the G or that obtained the G from the free
	// list.
	_Gdead // 6

	// _Genqueue_unused is currently unused.
	_Genqueue_unused // 7

	// _Gcopystack means this goroutine's stack is being moved. It
	// is not executing user code and is not on a run queue. The
	// stack is owned by the goroutine that put it in _Gcopystack.
	_Gcopystack // 8

	// _Gscan combined with one of the above states other than
	// _Grunning indicates that GC is scanning the stack. The
	// goroutine is not executing user code and the stack is owned
	// by the goroutine that set the _Gscan bit.
	//
	// _Gscanrunning is different: it is used to briefly block
	// state transitions while GC signals the G to scan its own
	// stack. This is otherwise like _Grunning.
	//
	// atomicstatus&~Gscan gives the state the goroutine will
	// return to when the scan completes.
	_Gscan         = 0x1000
	_Gscanrunnable = _Gscan + _Grunnable // 0x1001
	_Gscanrunning  = _Gscan + _Grunning  // 0x1002
	_Gscansyscall  = _Gscan + _Gsyscall  // 0x1003
	_Gscanwaiting  = _Gscan + _Gwaiting  // 0x1004
)

从注释中,我们可以看出:

_Gidle:表示刚刚分配了Goroutine内存但没有进行初始化,此时状态为空闲状态。这个我们可以从代码看出:

// 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.
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
	_g_ := getg()
	...
	//从回收列表中查找goroutine,避免重复创建
	newg := gfget(_p_)
	if newg == nil {
		//回收列表中不存在可用goroutine,则重新分配一个goroutine结构及其栈空间,栈空间使用内核分配
		newg = malg(_StackMin)
		//设置状态为死亡状态
		casgstatus(newg, _Gidle, _Gdead)
		//加到g列表中
		allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
	}
	...
	casgstatus(newg, _Gdead, _Grunnable)
	...
	//将go routine放进p任务队列
	runqput(_p_, newg, true)

	//由空闲的p,直接唤醒新创建的go routine
	if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {
		wakep()
	}
    ...
}
func allgadd(gp *g) {
	if readgstatus(gp) == _Gidle {
		throw("allgadd: bad status Gidle")
	}

	println("allgadd allgs:", allgs, len(allgs), gp)
	lock(&allglock)
	allgs = append(allgs, gp)
	allglen = uintptr(len(allgs))
	unlock(&allglock)
}

从上面代码可以看出:

刚分配内存的Goroutine状态为空闲状态_Gidle,然后切换状态是死亡状态_Gdead,然后刚创建的Goroutine放入Goroutine列表中,然后切换状态程可运行状态_Grunnable,然后把新创建的Goroutine放入p的本地队列中,等待执行。

总结:新建的Goroutine才会出于空闲状态_Gidle,而且时间特别短。

 

_Grunnable:可运行状态,在p的本地队列中等待执行。

// Mark gp ready to run.
func ready(gp *g, traceskip int, next bool) {
	...
	// status is Gwaiting or Gscanwaiting, make Grunnable and put on runq
	casgstatus(gp, _Gwaiting, _Grunnable)
	runqput(_g_.m.p.ptr(), gp, next)
	...
}
func findrunnable() (gp *g, inheritTime bool) {
	...
	// poll network
	if netpollinited() && atomic.Load(&netpollWaiters) > 0 && atomic.Xchg64(&sched.lastpoll, 0) != 0 {
        ...
		list := netpoll(true) // block until new work is available
		atomic.Store64(&sched.lastpoll, uint64(nanotime()))
		if !list.empty() {
    			...
				casgstatus(gp, _Gwaiting, _Grunnable)
				...
				return gp, false
			}
			injectglist(&list)
		}
	}
    ...
}
// Injects the list of runnable G's into the scheduler and clears glist.
// Can run concurrently with GC.
func injectglist(glist *gList) {
	...
	lock(&sched.lock)
	var n int
	for n = 0; !glist.empty(); n++ {
		gp := glist.pop()
		casgstatus(gp, _Gwaiting, _Grunnable)
		globrunqput(gp)
	}
	...
}
func goschedImpl(gp *g) {
    ...
	casgstatus(gp, _Grunning, _Grunnable)
    ...
}
// exitsyscall slow path on g0.
// Failed to acquire P, enqueue gp as runnable.
//
//go:nowritebarrierrec
func exitsyscall0(gp *g) {
	...
	casgstatus(gp, _Gsyscall, _Grunnable)
	...
}
// 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.
func procresize(nprocs int32) *p {
	...
	for i := nprocs; i < old; i++ {
		...
		if gp := p.gcBgMarkWorker.ptr(); gp != nil {
			casgstatus(gp, _Gwaiting, _Grunnable)
			...
			globrunqput(gp)
			...
		}
		...
	}
	...
}
// findRunnableGCWorker returns the background mark worker for _p_ if it
// should be run. This must only be called when gcBlackenEnabled != 0.
func (c *gcControllerState) findRunnableGCWorker(_p_ *p) *g {
	...
	casgstatus(gp, _Gwaiting, _Grunnable)
	...
}
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
	...
	casgstatus(newg, _Gdead, _Grunnable)
	...
}

涉及到切换到_Grunnable的代码点如上所示,可以看出有4中切换:

1、_Gdead -> _Grunnable 

2、_Gsyscall -> _Grunnable   

3、_Gwaiting -> _Grunnable  

第一种,上文已经提及到,新创建的Goroutine会从_Gdead切换到_Grunnable 

接下来,我们分析什么时机会触发_Gwaiting -> _Grunnable 、_Gsyscall -> _Grunnable和_Grunning -> _Grunnable  的状态切换。

第二种,_Gsyscall -> _Grunnable

跟踪 exitsyscall0函数:

exitsyscall -> exitsyscall0
//lock_sema.go
func notetsleepg(n *note, ns int64) bool {
	...
	entersyscallblock()
	ok := notetsleep_internal(n, ns, nil, 0)
	exitsyscall()
	return ok
}
//lock_futex.go
func notetsleepg(n *note, ns int64) bool {
	...
	entersyscallblock()
	ok := notetsleep_internal(n, ns)
	exitsyscall()
	return ok
}

总结:调用了阻塞的系统调用后会进入_Gsyscall,从阻塞系统调用唤醒后,会从_Gsyscall切换到_Grunnable。

func park_m(gp *g) {
	...
	casgstatus(gp, _Grunning, _Gwaiting)
	dropg()
	if _g_.m.waitunlockf != nil {
		...
		if !ok {
			if trace.enabled {
				traceGoUnpark(gp, 2)
			}
			casgstatus(gp, _Gwaiting, _Grunnable)
			execute(gp, true) // Schedule it back, never returns.
		}
	}
	schedule()
}

gopark -> park_m

触发gopark的点比较多,如下:

//*gopark
//chansend(chan.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//chanrecv(chan.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//gcBgMarkWorker(mgc.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//notetsleepg(lock_js.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//init(lock_js.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//handleEvent(lock_js.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//netpollblock(netpoll.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//main(proc.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//goparkunlock(proc.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//block(select.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//selectgo(select.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//notetsleepg(lock_sema.go -> (entersyscallblock - notetsleep_internal - exitsyscall)
//notetsleepg(lock_futext.go -> (entersyscallblock - notetsleep_internal - exitsyscall)

可以看出,基本是调用了阻塞接口后,会触发gopark进行状态切换,从这里也可以看的出,有哪些是阻塞行操作。

func park_m(gp *g) {
	_g_ := getg()

	if trace.enabled {
		traceGoPark(_g_.m.waittraceev, _g_.m.waittraceskip)
	}

	casgstatus(gp, _Grunning, _Gwaiting)
	dropg()

	if _g_.m.waitunlockf != nil {
		fn := *(*func(*g, unsafe.Pointer) bool)(unsafe.Pointer(&_g_.m.waitunlockf))
		ok := fn(gp, _g_.m.waitlock)
		_g_.m.waitunlockf = nil
		_g_.m.waitlock = nil
		if !ok {
			if trace.enabled {
				traceGoUnpark(gp, 2)
			}
			casgstatus(gp, _Gwaiting, _Grunnable)
			execute(gp, true) // Schedule it back, never returns.
		}
	}
	schedule()
}

总结:先从_Grunning切换_Gwaiting,再执行阻塞性操作,阻塞行操作唤醒后,切换成_Grunnable。

 

_Grunning:代表正在执行程序指令,即Goroutine正在运行,此时Goroutine不在p运行队列中,并取得m、p运行资源,即与m和p完成绑定关系,Goroutine完全使用所在m(线程)的栈空间。       

func newstack() {
	...
	if preempt {
		...
		// Synchronize with scang.
		casgstatus(gp, _Grunning, _Gwaiting)
		if gp.preemptscan {
			...
			casfrom_Gscanstatus(gp, _Gscanwaiting, _Gwaiting)
			// This clears gcscanvalid.
			casgstatus(gp, _Gwaiting, _Grunning)
			gp.stackguard0 = gp.stack.lo + _StackGuard
			gogo(&gp.sched) // never return
		}

		// Act like goroutine called runtime.Gosched.
		casgstatus(gp, _Gwaiting, _Grunning)
		gopreempt_m(gp) // never return
	}
	...
	casgstatus(gp, _Grunning, _Gcopystack)
	...
	copystack(gp, newsize, true)
	...
	casgstatus(gp, _Gcopystack, _Grunning)
	gogo(&gp.sched)
}
runtime·morestack_noctxt -> runtime·morestack -> newstack

 runtime·morestack -> newstack    至于何时调用morestack,这是由golang编译器和链接器完成的,我们可以查看stack.go的注释说明:

//stack.go
/*
Stack layout parameters.
Included both by runtime (compiled via 6c) and linkers (compiled via gcc).

The per-goroutine g->stackguard is set to point StackGuard bytes
above the bottom of the stack.  Each function compares its stack
pointer against g->stackguard to check for overflow.  To cut one
instruction from the check sequence for functions with tiny frames,
the stack is allowed to protrude StackSmall bytes below the stack
guard.  Functions with large frames don't bother with the check and
always call morestack.  The sequences are (for amd64, others are
similar):
*/

从注释可以看出,每个函数都会检查栈是否溢出。

我们在细看一下newstack,看看满足什么条件下会触发状态切换:

func newstack() {
	thisg := getg()
	// TODO: double check all gp. shouldn't be getg().
	if thisg.m.morebuf.g.ptr().stackguard0 == stackFork {
		throw("stack growth after fork")
	}
	if thisg.m.morebuf.g.ptr() != thisg.m.curg {
		print("runtime: newstack called from g=", hex(thisg.m.morebuf.g), "n"+"tm=", thisg.m, " m->curg=", thisg.m.curg, " m->g0=", thisg.m.g0, " m->gsignal=", thisg.m.gsignal, "n")
		morebuf := thisg.m.morebuf
		traceback(morebuf.pc, morebuf.sp, morebuf.lr, morebuf.g.ptr())
		throw("runtime: wrong goroutine in newstack")
	}

	gp := thisg.m.curg

	if thisg.m.curg.throwsplit {
		// Update syscallsp, syscallpc in case traceback uses them.
		morebuf := thisg.m.morebuf
		gp.syscallsp = morebuf.sp
		gp.syscallpc = morebuf.pc
		pcname, pcoff := "(unknown)", uintptr(0)
		f := findfunc(gp.sched.pc)
		if f.valid() {
			pcname = funcname(f)
			pcoff = gp.sched.pc - f.entry
		}
		print("runtime: newstack at ", pcname, "+", hex(pcoff),
			" sp=", hex(gp.sched.sp), " stack=[", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]n",
			"tmorebuf={pc:", hex(morebuf.pc), " sp:", hex(morebuf.sp), " lr:", hex(morebuf.lr), "}n",
			"tsched={pc:", hex(gp.sched.pc), " sp:", hex(gp.sched.sp), " lr:", hex(gp.sched.lr), " ctxt:", gp.sched.ctxt, "}n")

		thisg.m.traceback = 2 // Include runtime frames
		traceback(morebuf.pc, morebuf.sp, morebuf.lr, gp)
		throw("runtime: stack split at bad time")
	}

	morebuf := thisg.m.morebuf
	thisg.m.morebuf.pc = 0
	thisg.m.morebuf.lr = 0
	thisg.m.morebuf.sp = 0
	thisg.m.morebuf.g = 0

	// NOTE: stackguard0 may change underfoot, if another thread
	// is about to try to preempt gp. Read it just once and use that same
	// value now and below.
	preempt := atomic.Loaduintptr(&gp.stackguard0) == stackPreempt

	// Be conservative about where we preempt.
	// We are interested in preempting user Go code, not runtime code.
	// If we're holding locks, mallocing, or preemption is disabled, don't
	// preempt.
	// This check is very early in newstack so that even the status change
	// from Grunning to Gwaiting and back doesn't happen in this case.
	// That status change by itself can be viewed as a small preemption,
	// because the GC might change Gwaiting to Gscanwaiting, and then
	// this goroutine has to wait for the GC to finish before continuing.
	// If the GC is in some way dependent on this goroutine (for example,
	// it needs a lock held by the goroutine), that small preemption turns
	// into a real deadlock.
	if preempt {
		if thisg.m.locks != 0 || thisg.m.mallocing != 0 || thisg.m.preemptoff != "" || thisg.m.p.ptr().status != _Prunning {
			// Let the goroutine keep running for now.
			// gp->preempt is set, so it will be preempted next time.
			gp.stackguard0 = gp.stack.lo + _StackGuard
			gogo(&gp.sched) // never return
		}
	}

	if gp.stack.lo == 0 {
		throw("missing stack in newstack")
	}
	sp := gp.sched.sp
	if sys.ArchFamily == sys.AMD64 || sys.ArchFamily == sys.I386 || sys.ArchFamily == sys.WASM {
		// The call to morestack cost a word.
		sp -= sys.PtrSize
	}
	if stackDebug >= 1 || sp < gp.stack.lo {
		print("runtime: newstack sp=", hex(sp), " stack=[", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]n",
			"tmorebuf={pc:", hex(morebuf.pc), " sp:", hex(morebuf.sp), " lr:", hex(morebuf.lr), "}n",
			"tsched={pc:", hex(gp.sched.pc), " sp:", hex(gp.sched.sp), " lr:", hex(gp.sched.lr), " ctxt:", gp.sched.ctxt, "}n")
	}
	if sp < gp.stack.lo {
		print("runtime: gp=", gp, ", goid=", gp.goid, ", gp->status=", hex(readgstatus(gp)), "n ")
		print("runtime: split stack overflow: ", hex(sp), " < ", hex(gp.stack.lo), "n")
		throw("runtime: split stack overflow")
	}

	if preempt {
		if gp == thisg.m.g0 {
			throw("runtime: preempt g0")
		}
		if thisg.m.p == 0 && thisg.m.locks == 0 {
			throw("runtime: g is running but p is not")
		}
		// Synchronize with scang.
		casgstatus(gp, _Grunning, _Gwaiting)
		if gp.preemptscan {
			for !castogscanstatus(gp, _Gwaiting, _Gscanwaiting) {
				// Likely to be racing with the GC as
				// it sees a _Gwaiting and does the
				// stack scan. If so, gcworkdone will
				// be set and gcphasework will simply
				// return.
			}
			if !gp.gcscandone {
				// gcw is safe because we're on the
				// system stack.
				gcw := &gp.m.p.ptr().gcw
				scanstack(gp, gcw)
				gp.gcscandone = true
			}
			gp.preemptscan = false
			gp.preempt = false
			casfrom_Gscanstatus(gp, _Gscanwaiting, _Gwaiting)
			// This clears gcscanvalid.
			casgstatus(gp, _Gwaiting, _Grunning)
			gp.stackguard0 = gp.stack.lo + _StackGuard
			gogo(&gp.sched) // never return
		}

		// Act like goroutine called runtime.Gosched.
		casgstatus(gp, _Gwaiting, _Grunning)
		gopreempt_m(gp) // never return
	}

	// Allocate a bigger segment and move the stack.
	oldsize := gp.stack.hi - gp.stack.lo
	newsize := oldsize * 2
	if newsize > maxstacksize {
		print("runtime: goroutine stack exceeds ", maxstacksize, "-byte limitn")
		throw("stack overflow")
	}

	// The goroutine must be executing in order to call newstack,
	// so it must be Grunning (or Gscanrunning).
	casgstatus(gp, _Grunning, _Gcopystack)

	// The concurrent GC will not scan the stack while we are doing the copy since
	// the gp is in a Gcopystack status.
	copystack(gp, newsize, true)
	if stackDebug >= 1 {
		print("stack grow donen")
	}
	casgstatus(gp, _Gcopystack, _Grunning)
	gogo(&gp.sched)
}

总结:golang每个函数都会进行栈是否溢出检查,栈空间需要扩容时会newstack,此时会检查弱抢占式gc,会出发Goroutine状态切换_Grunning -> _Gwaiting -> _Grunning。

func writeheapdump_m(fd uintptr) {
	_g_ := getg()
	casgstatus(_g_.m.curg, _Grunning, _Gwaiting)
	...
	updatememstats()
	...
	mdump()
	casgstatus(_g_.m.curg, _Gwaiting, _Grunning)
}
runtime_debug_WriteHeapDump -> writeheapdump_m

当将堆信息、gc roots、线程、finalizers等信息dump到文件时会出发状态切换。

//gc相关操作
func markroot(gcw *gcWork, i uint32) {
	...
	switch {
	...
	default:
		...
		systemstack(func() {
			...
			selfScan := gp == userG && readgstatus(userG) == _Grunning
			if selfScan {
				casgstatus(userG, _Grunning, _Gwaiting)
				userG.waitreason = waitReasonGarbageCollectionScan
			}
			...
			scang(gp, gcw)
			if selfScan {
				casgstatus(userG, _Gwaiting, _Grunning)
			}
		})
	}
}
//gc相关操作
func gcAssistAlloc1(gp *g, scanWork int64) {
	...
	casgstatus(gp, _Grunning, _Gwaiting)
	gp.waitreason = waitReasonGCAssistMarking
	gcw := &getg().m.p.ptr().gcw
	workDone := gcDrainN(gcw, scanWork)
	casgstatus(gp, _Gwaiting, _Grunning)
	...
}
//gc相关操作
func gcMarkDone() {
	...
	gcMarkDoneFlushed = 0
	systemstack(func() {
		...
		casgstatus(gp, _Grunning, _Gwaiting)
		forEachP(func(_p_ *p) {
			...
		})
		casgstatus(gp, _Gwaiting, _Grunning)
	})
	...
}
//gc相关操作
func gcMarkTermination(nextTriggerRatio float64) {
	...
	casgstatus(gp, _Grunning, _Gwaiting)
	gp.waitreason = waitReasonGarbageCollection
	...
	systemstack(func() {
		gcMark(startTime)
		// Must return immediately.
		// The outer function's stack may have moved
		// during gcMark (it shrinks stacks, including the
		// outer function's stack), so we must not refer
		// to any of its variables. Return back to the
		// non-system stack to pick up the new addresses
		// before continuing.
	})

	systemstack(func() {
		...
		setGCPhase(_GCoff)
		gcSweep(work.mode)
	})

	_g_.m.traceback = 0
	casgstatus(gp, _Gwaiting, _Grunning)
	...
}
//gc相关操作
func gcBgMarkWorker(_p_ *p) {
	...
	for {
		...
		systemstack(func() {
			casgstatus(gp, _Grunning, _Gwaiting)
			...
			casgstatus(gp, _Gwaiting, _Grunning)
		})
		...
	}
}
总结:以上均是gc操作,进行相关gc操作时,都会将Goroutine先切换成_Gwaiting,gc操作完成后再切换会_Grunning。

由此可见,gc操作对应用性能的影响是很大的,毕竟gc需要停止所有正在运行的gc,进行完gc操作再切换回去,虽然golang gc部分已经做了很大的优化,gc耗时也已经很短,但编写golang程序时,仍需注意避免过于动态分配内存,导致过多的gc。针对这个问题,有很多解决方案,比较通用的是使用bufferpool,一次性申请大内存,每次需要动态内存时,从bufferpool取,由于bufferpool一直有引用,因此不会触发gc回收。此做法虽然极大降低了gc带来了性能问题,但也需要额外锁操作的消耗。

func execute(gp *g, inheritTime bool) {
	...
	casgstatus(gp, _Grunnable, _Grunning)
	...
	gogo(&gp.sched)
}

总结:Goroutine得到调度机会后会完成从_Grunnable到_Grunning的切换。

 

_Gsyscall:也说明Goroutine,但不是执行用户层代码,也是正在执行系统调用,此时Goroutine不在p运行队列中,完全拥有m的栈空间。

func reentersyscall(pc, sp uintptr) {
	_g_ := getg()
	println("reentersyscall: ", _g_.goid)
    ...
	casgstatus(_g_, _Grunning, _Gsyscall)
	...
}
entersyscall -> reentersyscall
func entersyscallblock() {
	...
	casgstatus(_g_, _Grunning, _Gsyscall)
	...
}

_Gwaiting:此时Goroutine没有执行用户代码,即不在p运行队列,只是某个时刻打个标。

上面提到的gc操作和dumpheap操作就涉及_Grunning到_Gwaiting和_Gwaiting到_Grunning的切换。

涉及切换状态的代码如下:

_Grunning -> _Gwaiting 
newstatck
writeheapdump_m
gcMarkTermination
gcBgMarkWorker
park_m
markroot
gcAssistAlloc1

_Gwaiting -> _Grunning
newstack
writeheapdump_m
gcMarkDone
gcMarkTermination
gcBgMarkWorker
markroot
gcAssistAlloc1
-----------------------

_Grunning -> _Gsyscall 
reentersyscall
entersyscallblock

_Gsyscall -> _Grunning
exitsyscall
-----------------------

_Gsyscall -> _Grunnable 
exitsyscall0
-----------------------

_Grunning -> _Gcopystack 
newstatck

_Gcopystack -> _Grunning
newstatck
-----------------------

_Gwaiting -> _Grunnable
ready
findrunnable
injectglist
schedule
park_m
procresize
checkdead
-----------------------

_Grunnable -> _Grunning
execute

_Grunning -> _Grunnable
lock -> gosched_m -> goschedImpl
Gosched -> gosched_m -> goschedImpl
goschedguarded -> goschedguarded_m -> goschedImpl
gopreempt_m -> goschedImpl


-----------------------
_Grunning -> _Gdead
goexit -> goexit1 -> goexit0
Goexit -> goexit1 -> goexit0

-----------------------

_Gdead -> _Gsyscall
needm

_Gsyscall -> _Gdead
dropm
-----------------------

_Gdead -> _Grunnable
newproc1

-----------------------

_Gidle -> _Gdead
oneNewExtraM
newproc1
-----------------------

汇总成一张状态切换图:

 

总结:

1、golang中的go语句实际上对应的代码是newproc,通过newproc会新创建一个Goroutine,分配内存空间,此时状态为_GIdle,然后切换为_Gdead,接着切换状态为_Grunnable并将新创建的Goroutine放入p的运行队列中,等待调度机会;

2、在适合的机会下,即存在空闲的p和空闲的m,会切换为_Grunning(execute),即运行Goroutine;

3、Goroutine的入口函数执行完毕,即从入口函数返回了,会调用goexit来执行真正的退出(释放锁、内存、绑定的m/p资源等);

4、正在运行的Goroutine调用了阻塞的系统调用,在调用系统调用之前,会从_Grunning切换到_Gsyscall(entersyscall),等从系统调用唤醒(notetsleep_internal)后。如果现在不存在可用的p,则会从_Gsyscall切换会_Grunning(exitsyscall),否则会切换为_Grunnable(exitsyscall);

5、needm/dropm是在使用cgo的情况才会涉及,这里不展开讨论;

6、在gc(垃圾回收器)进行gc操作时,需正对正在运行的Goroutine先从_Grunning切换到_Gwating,gc操作完毕后,再切换回_Grunning;

7、在进行dumpheap操作时,需正对正在运行的Goroutine先从_Grunning切换到_Gwating,dumpheap操作完毕后,再切换回_Grunning;

8、morestack/newstack,golang编译器/链接器会在每个函数加入这段代码,如果有必要扩容栈空间,则会执行栈空间扩容操作,扩容前,会先从_Grunning切换到_Gwating,扩容完成后,再切换回_Grunning;因为每个函数都会调用,所以gc在newstack加上了抢占式操作,也会先从_Grunning切换到_Gwating,操作完成后再切换回_Grunning(gopreempt_m)。这个机制在一定程度上避免某个Goroutine真的会出现饿死的情况;但是如果代码执行了for {//没有任务函数调用},这种害死整个m的情况,谁也没更好办法;

9、正在运行的Goroutine调用了阻塞操作,会导致gopark,gopark会释放当前绑定的m,即放弃运行继续,从_Grunning切换程_Gwaiting,调用gopark的情况如下:

//chansend(chan.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//chanrecv(chan.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//gcBgMarkWorker(mgc.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//notetsleepg(lock_js.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//init(lock_js.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//handleEvent(lock_js.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//netpollblock(netpoll.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//main(proc.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//goparkunlock(proc.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//block(select.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go
//selectgo(select.go -> gopark(proc.go -> park_m(proc.go -> schedule(proc.go

10、调用goready情况如下:

//goready
//goready(proc.go -> ready(proc.go -> runqput(proc.go -> wakeup(proc.go
//send(chan.go -> goready(proc.go -> ready(proc.go -> runqput(proc.go -> wakeup(proc.go
//closechan(chan.go -> goready(proc.go -> ready(proc.go -> runqput(proc.go -> wakeup(proc.go
//recv(chan.go -> goready(proc.go -> ready(proc.go -> runqput(proc.go -> wakeup(proc.go
//poll_runtime_pollSetDeadline(netpoll.go -> netpollgoready(netpoll.go -> goready(proc.go  -> ready(proc.go -> runqput(proc.go -> wakeup(proc.go
//poll_runtime_pollUnblock(netpoll.go -> netpollgoready(netpoll.go -> goready(proc.go -> ready(proc.go -> runqput(proc.go -> wakeup(proc.go
//netpolldeadlineimpl(netpoll.go -> netpollgoready(netpoll.go -> goready(proc.go -> ready(proc.go -> runqput(proc.go -> wakeup(proc.go
//notewakeup(lock_js.go -> goready(proc.go -> ready(proc.go -> runqput(proc.go -> wakeup(proc.go
//checkTimeouts(lock_js.go -> goready(proc.go -> ready(proc.go -> runqput(proc.go -> wakeup(proc.go
//beforeIdle(lock_js.go -> goready(proc.go -> ready(proc.go -> runqput(proc.go -> wakeup(proc.go
//goroutineReady(time.go -> goready(proc.go -> ready(proc.go -> runqput(proc.go -> wakeup(proc.go
//addtimerLocked(time.go -> goready(proc.go -> ready(proc.go -> runqput(proc.go -> wakeup(proc.go
//readyWithTime(sema.go -> goready(proc.go -> ready(proc.go -> runqput(proc.go -> wakeup(proc.go

11、schedule保证了不会退出;

12、deadlock会进行死锁检测;

本文就是就Goroutine状态切换进行说明,提到的p和m并没有细说,接下来需要研究p和m。

 

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/QQ1130141391/article/details/96350019
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2021-06-12 20:36:41
  • 阅读 ( 1172 )
  • 分类:Go

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢