点击上方蓝色“Go语言中文网”关注我们,领全套Go资料,每天学习 Go 语言

原文链接:https://juejin.im/post/5dafc241f265da5ba95c465d

golang 调度模型

模型总揽

c371057affd3b6a709aaf49660373fbf.png

核心实体

Goroutines (G)

golang 调度单元,golang 可以开启成千上万个 g,每个 g 可以理解为一个任务,等待被调度。其存储了 goroutine 的执行 stack 信息、goroutine 状态以及 goroutine 的任务函数等。g 只能感知到 p,下文说的 m 对其透明的。

OSThread (M)

系统线程,实际执行 g 的狠角色,但 m 并不维护 g 的状态,一切都是由幕后黑手 p 来控制。

Processor (P)

维护 m 执行时所需要的上下文,p 的个数通常和 cpu 核数一致(可以设置),代表 gorotine 的并发度。其维护了 g 的队列。

实体间的关系

一图胜千言,直接看这个经典的图856fb84a6fab99648471c73b7a4aa1bd.png

调度本质

即 schedule 函数,通过调度,放弃目前执行的 g,选择一个 g 来执行。选择算法不是本文重点,这里不做过多讲述。

切换时机

  • 会阻塞的系统调用,比如文件 io,网络 io;
  • time 系列定时操作;
  • go func 的时候, func 执行完的时候;
  • 管道读写阻塞的情况;
  • 垃圾回收之后。
  • 主动调用 runtime.Gosched()

调度时机分析

阻塞性系统调用

系统调用,如 read,golang 重写了所有系统调用,在系统调用加入了调度逻辑。

拿 read 举例

/usr/local/go/src/os/file.go:97

// Read reads up to len(b) bytes from the File.
// It returns the number of bytes read and an error, if any.
// EOF is signaled by a zero count with err set to io.EOF.
func (f *File) Read(b []byte) (n int, err error) {
if f == nil {
return 0, ErrInvalid
}
n, e := f.read(b)
if n == 0 && len(b) > 0 && e == nil {
return 0, io.EOF
}
if e != nil {
err = &PathError{"read", f.name, e}
}
return n, err
}

嵌套到几层,就不全部贴出来,跟到底是如下函数:

func read(fd int, p []byte) (n int, err error) {
var _p0 unsafe.Pointer
if len(p) > 0 {
_p0 = unsafe.Pointer(&p[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_READ, uintptr(fd), uintptr(_p0), uintptr(len(p)))
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}

func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)

Syscall 是汇编实现

TEXT    ·Syscall(SB),NOSPLIT,$0-56
BL runtime·entersyscall(SB)
MOVD a1+8(FP), R3
MOVD a2+16(FP), R4
MOVD a3+24(FP), R5
MOVD R0, R6
MOVD R0, R7
MOVD R0, R8
MOVD trap+0(FP), R9 // syscall entry
SYSCALL R9
BVC ok
MOVD $-1, R4
MOVD R4, r1+32(FP) // r1
MOVD R0, r2+40(FP) // r2
MOVD R3, err+48(FP) // errno
BL runtime·exitsyscall(SB)
RET
ok:
MOVD R3, r1+32(FP) // r1
MOVD R4, r2+40(FP) // r2
MOVD R0, err+48(FP) // errno
BL runtime·exitsyscall(SB)
RET

可以看到,进入系统调用时,是调用 entersyscall,当离开系统调用,会运行 exitsyscall

// Standard syscall entry used by the go syscall library and normal cgo calls.
//go:nosplit
func entersyscall(dummy int32) {
reentersyscall(getcallerpc(unsafe.Pointer(&dummy)), getcallersp(unsafe.Pointer(&dummy)))
}

func reentersyscall(pc, sp uintptr) {
_g_ := getg()

// Disable preemption because during this function g is in Gsyscall status,
// but can have inconsistent g->sched, do not let GC observe it.
_g_.m.locks++

// Entersyscall must not call any function that might split/grow the stack.
// (See details in comment above.)
// Catch calls that might, by replacing the stack guard with something that
// will trip any stack check and leaving a flag to tell newstack to die.
_g_.stackguard0 = stackPreempt
_g_.throwsplit = true

// Leave SP around for GC and traceback.
save(pc, sp)
_g_.syscallsp = sp
_g_.syscallpc = pc
casgstatus(_g_, _Grunning, _Gsyscall)
if _g_.syscallsp < _g_.stack.lo || _g_.stack.hi < _g_.syscallsp {
systemstack(func() {
print("entersyscall inconsistent ", hex(_g_.syscallsp), " [", hex(_g_.stack.lo), ",", hex(_g_.stack.hi), "]\n")
throw("entersyscall")
})
}

if trace.enabled {
systemstack(traceGoSysCall)
// systemstack itself clobbers g.sched.{pc,sp} and we might
// need them later when the G is genuinely blocked in a
// syscall
save(pc, sp)
}

if atomic.Load(&sched.sysmonwait) != 0 { // TODO: fast atomic
systemstack(entersyscall_sysmon)
save(pc, sp)
}

if _g_.m.p.ptr().runSafePointFn != 0 {
// runSafePointFn may stack split if run on this stack
systemstack(runSafePointFn)
save(pc, sp)
}

_g_.m.syscalltick = _g_.m.p.ptr().syscalltick
_g_.sysblocktraced = true
_g_.m.mcache = nil
_g_.m.p.ptr().m = 0
atomic.Store(&_g_.m.p.ptr().status, _Psyscall)
if sched.gcwaiting != 0 {
systemstack(entersyscall_gcwait)
save(pc, sp)
}

// Goroutines must not split stacks in Gsyscall status (it would corrupt g->sched).
// We set _StackGuard to StackPreempt so that first split stack check calls morestack.
// Morestack detects this case and throws.
_g_.stackguard0 = stackPreempt
_g_.m.locks--
}
_Gsyscall
_Gsyscall
  1. 当 m 执行完,调用 exitsyscall 重新和之前的 p 绑定,其中调度的还是 schedule 函数;
  2. sysmon 线程,发现该 p 一定时间没有执行,会其分配一个新的 m。此时进入调度。

time 定时类操作

都拿 time.Sleep 举例

// Sleep pauses the current goroutine for at least the duration d.
// A negative or zero duration causes Sleep to return immediately.
func Sleep(d Duration)
实际定义在runtime
// timeSleep puts the current goroutine to sleep for at least ns nanoseconds.
//go:linkname timeSleep time.Sleepfunc timeSleep(ns int64) {
if ns <= 0 {
return
}

t := getg().timer
if t == nil {
t = new(timer)
getg().timer = t
}
*t = timer{}
t.when = nanotime() + ns
t.f = goroutineReady
t.arg = getg()
lock(&timers.lock)
addtimerLocked(t)
goparkunlock(&timers.lock, "sleep", traceEvGoSleep, 2)
}

goparkunlock 最终调用 gopark

func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason string, traceEv byte, traceskip int) {
mp := acquirem()
gp := mp.curg
status := readgstatus(gp)
if status != _Grunning && status != _Gscanrunning {
throw("gopark: bad g status")
}
mp.waitlock = lock
mp.waitunlockf = *(*unsafe.Pointer)(unsafe.Pointer(&unlockf))
gp.waitreason = reason
mp.waittraceev = traceEv
mp.waittraceskip = traceskip
releasem(mp)
// can't do anything that might move the G between Ms here.
mcall(park_m)
}

mcall(fn) 是切换到 g0,让 g0 来调用 fn,这里我们看下 park_m 定义 park_m

func park_m(gp *g) {mcall(park_m)
_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 = nilif !ok {
if trace.enabled {
traceGoUnpark(gp, 2)
}
casgstatus(gp, _Gwaiting, _Grunnable)
execute(gp, true) // Schedule it back, never returns.
}
}
schedule()
}
_Gwaiting_Gwaiting

新起一个协程和退出

_GIdle_GDead_Gdead

管道阻塞

c 的实现
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if c == nil {
if !block {
returnfalse
}
gopark(nil, nil, "chan send (nil chan)", traceEvGoStop, 2)
throw("unreachable")
}

if debugChan {
print("chansend: chan=", c, "\n")
}

if raceenabled {
racereadpc(unsafe.Pointer(c), callerpc, funcPC(chansend))
}

........
// 省略无关代码
........

// Block on the channel. Some receiver will complete our operation for us.
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// No stack splits between assigning elem and enqueuing mysg
// on gp.waiting where copystack can find it.
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.selectdone = nil
mysg.c = c
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg)
goparkunlock(&c.lock, "chan send", traceEvGoBlockSend, 3)

// someone woke us up.
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
if gp.param == nil {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
gp.param = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
releaseSudog(mysg)
returntrue
}
goparkunlock->gopark

gc 之后

stw 之后,会重新选择 g 开始执行。此处不对垃圾回收做过多扩展。

主动调用 runtime.Gosched()

没有找到非要调用 runtime.Gosched 的场景,主要作用还是为了调试,学习 runtime 吧

// Gosched yields the processor, allowing other goroutines to run. It does not
// suspend the current goroutine, so execution resumes automatically.
//go:nosplit
func Gosched() {
mcall(gosched_m)
}

第一步就将环境切换到 g0,然后执行一个叫 gosched_m 的函数

// Gosched continuation on g0.
func gosched_m(gp *g) {
if trace.enabled {
traceGoSched()
}
goschedImpl(gp)
}

func goschedImpl(gp *g) {
status := readgstatus(gp)
if status&^_Gscan != _Grunning {
dumpgstatus(gp)
throw("bad g status")
}
casgstatus(gp, _Grunning, _Grunnable)
dropg()
lock(&sched.lock)
globrunqput(gp)
unlock(&sched.lock)

schedule()
}
_Grunnable

总结

golang 协程调度时机主要是阻塞性操作开始,结束。研究每个场景相关代码,即可对 golang 有更深的理解。这里也分享一个阅读源码的小经验,每次带着一个特定问题去寻找答案,比如本文的调度时机,后面再看调度算法,垃圾回收,这样每次能忽略无关因素,通过多个不同的主题,整个框架会越来越完善。

参考文章

  1. A complete journey with Goroutines: https://medium.com/@riteeksrivastava/a-complete-journey-with-goroutines-8472630c7f5c
  2. Go's work-stealing scheduler: https://rakyll.org/scheduler/

推荐阅读

  • xxx


喜欢本文的朋友,欢迎关注“Go语言中文网”:

926289ccf9a94b963959e53e7f2da487.png