golang异步协程调度

在1.14的go版本中,官方通过加入信号来进行协程的调度,后续就都支持了这种异步协程抢占,避免了早起的考栈调度时来检查是否执行超时的逻辑。本文简单来对比这种实现的原理。

调度代码
package main

import "fmt"

func main() {
	go func() {
		for i:=0;i>=0;i++{
			fmt.Println(i)
		}
	}()

	for{}
}

1.12版本对比

通过将该段代码放在1.12环境中运行。

 GOMAXPROCS=2  GODEBUG='schedtrace=1000,gctrace=1,scheddetail=1' go run main_sch.go > main_log

通过打开调试来观察,并且可以打开一个tail -f main_log来观察协程是否在输出i。

通过观察可以发现在运行一段时间之后,程序会阻塞住,此时观察调度器的输出信息

SCHED 22121ms: gomaxprocs=2 idleprocs=2 threads=16 spinningthreads=0 idlethreads=9 runqueue=0 gcwaiting=0 nmidlelocked=1 stopwait=0 sysmonwait=0
  P0: status=0 schedtick=10394 syscalltick=21045 m=-1 runqsize=0 gfreecnt=7
  P1: status=0 schedtick=8534 syscalltick=23086 m=-1 runqsize=0 gfreecnt=8
  M15: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M14: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M13: p=-1 curg=13 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M12: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M11: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M10: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M9: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M8: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M7: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=64
  M6: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M5: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M4: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M3: p=-1 curg=19 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M2: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 spinning=false blocked=false lockedg=-1
  M1: p=-1 curg=17 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=17
  M0: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  G1: status=4(semacquire) m=-1 lockedm=-1
  G17: status=6() m=1 lockedm=1
  G2: status=4(force gc (idle)) m=-1 lockedm=-1
  G3: status=4(GC sweep wait) m=-1 lockedm=-1
  G18: status=4(finalizer wait) m=-1 lockedm=-1
  G19: status=3() m=3 lockedm=-1
  G12: status=4(select) m=-1 lockedm=-1
  G13: status=3(chan send) m=13 lockedm=-1
  G14: status=6() m=-1 lockedm=-1
  G43: status=6() m=-1 lockedm=-1
  G16: status=6() m=-1 lockedm=-1
  G10: status=4(select) m=-1 lockedm=-1
  G11: status=4(select) m=-1 lockedm=-1
  G51: status=6() m=-1 lockedm=-1
  G65: status=4(chan receive) m=-1 lockedm=-1
  G4: status=4(GC worker (idle)) m=-1 lockedm=-1
  G5: status=4(GC worker (idle)) m=-1 lockedm=-1
  G7: status=4(select) m=-1 lockedm=-1
  G8: status=4(select) m=-1 lockedm=-1
  G45: status=6() m=-1 lockedm=-1
  G42: status=6() m=-1 lockedm=-1
  G41: status=6() m=-1 lockedm=-1
  G6: status=4(select) m=-1 lockedm=-1
  G39: status=6() m=-1 lockedm=-1
  G9: status=4(select) m=-1 lockedm=-1
  G38: status=6() m=-1 lockedm=-1
  G59: status=6() m=-1 lockedm=-1
  G53: status=6() m=-1 lockedm=-1
  G64: status=4(select) m=-1 lockedm=7
  G55: status=6() m=-1 lockedm=-1
  G61: status=6() m=-1 lockedm=-1
  G57: status=6() m=-1 lockedm=-1
  G58: status=6() m=-1 lockedm=-1
SCHED 21109ms: gomaxprocs=2 idleprocs=0 threads=5 spinningthreads=0 idlethreads=2 runqueue=0 gcwaiting=1 nmidlelocked=0 stopwait=1 sysmonwait=0
  P0: status=3 schedtick=316559 syscalltick=954340 m=2 runqsize=0 gfreecnt=0
  P1: status=1 schedtick=3 syscalltick=4 m=0 runqsize=0 gfreecnt=0
  M4: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M3: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M2: p=0 curg=18 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M1: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 spinning=false blocked=false lockedg=-1
  M0: p=1 curg=1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  G1: status=2(chan receive) m=0 lockedm=-1
  G2: status=4(force gc (idle)) m=-1 lockedm=-1
  G3: status=4(GC sweep wait) m=-1 lockedm=-1
  G17: status=4(finalizer wait) m=-1 lockedm=-1
  G18: status=2() m=2 lockedm=-1
  G4: status=4(GC worker (idle)) m=-1 lockedm=-1
  G5: status=4(GC worker (idle)) m=-1 lockedm=-1
SCHED 23123ms: gomaxprocs=2 idleprocs=2 threads=16 spinningthreads=0 idlethreads=9 runqueue=0 gcwaiting=0 nmidlelocked=1 stopwait=0 sysmonwait=0
  P0: status=0 schedtick=10394 syscalltick=21045 m=-1 runqsize=0 gfreecnt=7
  P1: status=0 schedtick=8534 syscalltick=23086 m=-1 runqsize=0 gfreecnt=8
  M15: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M14: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M13: p=-1 curg=13 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M12: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M11: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M10: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M9: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M8: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M7: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=64
  M6: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M5: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M4: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M3: p=-1 curg=19 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M2: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 spinning=false blocked=false lockedg=-1
  M1: p=-1 curg=17 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=17
  M0: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  G1: status=4(semacquire) m=-1 lockedm=-1
  G17: status=6() m=1 lockedm=1
  G2: status=4(force gc (idle)) m=-1 lockedm=-1
  G3: status=4(GC sweep wait) m=-1 lockedm=-1
  G18: status=4(finalizer wait) m=-1 lockedm=-1
  G19: status=3() m=3 lockedm=-1
  G12: status=4(select) m=-1 lockedm=-1
  G13: status=3(chan send) m=13 lockedm=-1
  G14: status=6() m=-1 lockedm=-1
  G43: status=6() m=-1 lockedm=-1
  G16: status=6() m=-1 lockedm=-1
  G10: status=4(select) m=-1 lockedm=-1
  G11: status=4(select) m=-1 lockedm=-1
  G51: status=6() m=-1 lockedm=-1
  G65: status=4(chan receive) m=-1 lockedm=-1
  G4: status=4(GC worker (idle)) m=-1 lockedm=-1
  G5: status=4(GC worker (idle)) m=-1 lockedm=-1
  G7: status=4(select) m=-1 lockedm=-1
  G8: status=4(select) m=-1 lockedm=-1
  G45: status=6() m=-1 lockedm=-1
  G42: status=6() m=-1 lockedm=-1
  G41: status=6() m=-1 lockedm=-1
  G6: status=4(select) m=-1 lockedm=-1
  G39: status=6() m=-1 lockedm=-1
  G9: status=4(select) m=-1 lockedm=-1
  G38: status=6() m=-1 lockedm=-1
  G59: status=6() m=-1 lockedm=-1
  G53: status=6() m=-1 lockedm=-1
  G64: status=4(select) m=-1 lockedm=7
  G55: status=6() m=-1 lockedm=-1
  G61: status=6() m=-1 lockedm=-1
  G57: status=6() m=-1 lockedm=-1
  G58: status=6() m=-1 lockedm=-1
SCHED 22117ms: gomaxprocs=2 idleprocs=0 threads=5 spinningthreads=0 idlethreads=2 runqueue=0 gcwaiting=1 nmidlelocked=0 stopwait=1 sysmonwait=0
  P0: status=3 schedtick=316559 syscalltick=954340 m=2 runqsize=0 gfreecnt=0
  P1: status=1 schedtick=3 syscalltick=4 m=0 runqsize=0 gfreecnt=0
  M4: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M3: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M2: p=0 curg=18 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M1: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 spinning=false blocked=false lockedg=-1
  M0: p=1 curg=1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  G1: status=2(chan receive) m=0 lockedm=-1
  G2: status=4(force gc (idle)) m=-1 lockedm=-1
  G3: status=4(GC sweep wait) m=-1 lockedm=-1
  G17: status=4(finalizer wait) m=-1 lockedm=-1
  G18: status=2() m=2 lockedm=-1
  G4: status=4(GC worker (idle)) m=-1 lockedm=-1
  G5: status=4(GC worker (idle)) m=-1 lockedm=-1

梳理,可以发现G18在绑定到M2运行的时候一直都是运行,M2在被绑定运行的时候就一直运行,并且其他协程都已经停止了,因为此时启动了垃圾回收,强制等待停止所有的运行协程,但是源代码中的main中只有一个空的for循环并且里面无函数调用等栈变更,导致该协程一直无法完成停止,最终程序会堵住。

1.16版本跟踪对比

同理该函数在1.16版本中运行,发现main_log会一直的打印数据并且都不停止。

终端中输出的调试信息如下。

SCHED 25158ms: gomaxprocs=2 idleprocs=2 threads=13 spinningthreads=0 idlethreads=6 runqueue=0 gcwaiting=0 nmidlelocked=1 stopwait=0 sysmonwait=0
  P0: status=0 schedtick=1932 syscalltick=6567 m=-1 runqsize=0 gfreecnt=1 timerslen=0
  P1: status=0 schedtick=1646 syscalltick=10943 m=-1 runqsize=0 gfreecnt=21 timerslen=0
  M12: p=-1 curg=68 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M11: p=-1 curg=30 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M10: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M9: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M8: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M7: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M6: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M5: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M4: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M3: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M2: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=2 dying=0 spinning=false blocked=false lockedg=-1
  M1: p=-1 curg=17 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=17
  M0: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=87
  G1: status=4(semacquire) m=-1 lockedm=-1
  G17: status=6() m=1 lockedm=1
  G2: status=4(force gc (idle)) m=-1 lockedm=-1
  G3: status=4(GC sweep wait) m=-1 lockedm=-1
  G4: status=4(GC scavenge wait) m=-1 lockedm=-1
  G5: status=4(finalizer wait) m=-1 lockedm=-1
  G47: status=6() m=-1 lockedm=-1
  G65: status=6() m=-1 lockedm=-1
  G8: status=6() m=-1 lockedm=-1
  G38: status=6() m=-1 lockedm=-1
  G40: status=6() m=-1 lockedm=-1
  G36: status=6() m=-1 lockedm=-1
  G37: status=6() m=-1 lockedm=-1
  G43: status=6() m=-1 lockedm=-1
  G41: status=6() m=-1 lockedm=-1
  G33: status=4(select) m=-1 lockedm=-1
  G35: status=6() m=-1 lockedm=-1
  G18: status=4(GC worker (idle)) m=-1 lockedm=-1
  G19: status=4(GC worker (idle)) m=-1 lockedm=-1
  G29: status=4(select) m=-1 lockedm=-1
  G32: status=4(select) m=-1 lockedm=-1
  G44: status=6() m=-1 lockedm=-1
  G31: status=4(select) m=-1 lockedm=-1
  G30: status=3(chan send) m=11 lockedm=-1
  G45: status=6() m=-1 lockedm=-1
  G51: status=4(select) m=-1 lockedm=-1
  G50: status=4(select) m=-1 lockedm=-1
  G52: status=4(select) m=-1 lockedm=-1
  G53: status=6() m=-1 lockedm=-1
  G84: status=6() m=-1 lockedm=-1
  G55: status=6() m=-1 lockedm=-1
  G68: status=3() m=12 lockedm=-1
  G48: status=6() m=-1 lockedm=-1
  G87: status=4(select) m=-1 lockedm=0
  G59: status=6() m=-1 lockedm=-1
  G60: status=6() m=-1 lockedm=-1
  G61: status=6() m=-1 lockedm=-1
  G62: status=6() m=-1 lockedm=-1
  G63: status=6() m=-1 lockedm=-1
  G64: status=6() m=-1 lockedm=-1
  G88: status=4(chan receive) m=-1 lockedm=-1
gc 9 @24.471s 0%: 0.057+0.20+0.020 ms clock, 0.11+0.10/0.034/0+0.040 ms cpu, 4->4->0 MB, 5 MB goal, 2 P
SCHED 25050ms: gomaxprocs=2 idleprocs=0 threads=5 spinningthreads=1 idlethreads=2 runqueue=1 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
  P0: status=1 schedtick=5517 syscalltick=2710834 m=0 runqsize=0 gfreecnt=0 timerslen=1
  P1: status=0 schedtick=3750 syscalltick=2012100 m=-1 runqsize=0 gfreecnt=0 timerslen=0
  M4: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M3: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=true blocked=true lockedg=-1
  M2: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M1: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=2 dying=0 spinning=false blocked=false lockedg=-1
  M0: p=0 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 spinning=false blocked=false lockedg=-1
  G1: status=1(preempted) m=-1 lockedm=-1
  G2: status=4(force gc (idle)) m=-1 lockedm=-1
  G3: status=4(GC sweep wait) m=-1 lockedm=-1
  G4: status=4(sleep) m=-1 lockedm=-1
  G17: status=4(finalizer wait) m=-1 lockedm=-1
  G18: status=1(GC assist marking) m=-1 lockedm=-1
  G5: status=4(GC worker (idle)) m=-1 lockedm=-1
  G6: status=4(GC worker (idle)) m=-1 lockedm=-1
SCHED 26168ms: gomaxprocs=2 idleprocs=2 threads=13 spinningthreads=0 idlethreads=6 runqueue=0 gcwaiting=0 nmidlelocked=1 stopwait=0 sysmonwait=0
  P0: status=0 schedtick=1932 syscalltick=6567 m=-1 runqsize=0 gfreecnt=1 timerslen=0
  P1: status=0 schedtick=1646 syscalltick=10943 m=-1 runqsize=0 gfreecnt=21 timerslen=0
  M12: p=-1 curg=68 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M11: p=-1 curg=30 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M10: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M9: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M8: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M7: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M6: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M5: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M4: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M3: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M2: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=2 dying=0 spinning=false blocked=false lockedg=-1
  M1: p=-1 curg=17 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=17
  M0: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=87
  G1: status=4(semacquire) m=-1 lockedm=-1
  G17: status=6() m=1 lockedm=1
  G2: status=4(force gc (idle)) m=-1 lockedm=-1
  G3: status=4(GC sweep wait) m=-1 lockedm=-1
  G4: status=4(GC scavenge wait) m=-1 lockedm=-1
  G5: status=4(finalizer wait) m=-1 lockedm=-1
  G47: status=6() m=-1 lockedm=-1
  G65: status=6() m=-1 lockedm=-1
  G8: status=6() m=-1 lockedm=-1
  G38: status=6() m=-1 lockedm=-1
  G40: status=6() m=-1 lockedm=-1
  G36: status=6() m=-1 lockedm=-1
  G37: status=6() m=-1 lockedm=-1
  G43: status=6() m=-1 lockedm=-1
  G41: status=6() m=-1 lockedm=-1
  G33: status=4(select) m=-1 lockedm=-1
  G35: status=6() m=-1 lockedm=-1
  G18: status=4(GC worker (idle)) m=-1 lockedm=-1
  G19: status=4(GC worker (idle)) m=-1 lockedm=-1
  G29: status=4(select) m=-1 lockedm=-1
  G32: status=4(select) m=-1 lockedm=-1
  G44: status=6() m=-1 lockedm=-1
  G31: status=4(select) m=-1 lockedm=-1
  G30: status=3(chan send) m=11 lockedm=-1
  G45: status=6() m=-1 lockedm=-1
  G51: status=4(select) m=-1 lockedm=-1
  G50: status=4(select) m=-1 lockedm=-1
  G52: status=4(select) m=-1 lockedm=-1
  G53: status=6() m=-1 lockedm=-1
  G84: status=6() m=-1 lockedm=-1
  G55: status=6() m=-1 lockedm=-1
  G68: status=3() m=12 lockedm=-1
  G48: status=6() m=-1 lockedm=-1
  G87: status=4(select) m=-1 lockedm=0
  G59: status=6() m=-1 lockedm=-1
  G60: status=6() m=-1 lockedm=-1
  G61: status=6() m=-1 lockedm=-1
  G62: status=6() m=-1 lockedm=-1
  G63: status=6() m=-1 lockedm=-1
  G64: status=6() m=-1 lockedm=-1
  G88: status=4(chan receive) m=-1 lockedm=-1
SCHED 26050ms: gomaxprocs=2 idleprocs=0 threads=5 spinningthreads=0 idlethreads=2 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
  P0: status=2 schedtick=5605 syscalltick=2846152 m=-1 runqsize=0 gfreecnt=0 timerslen=0
  P1: status=1 schedtick=3846 syscalltick=2090028 m=4 runqsize=0 gfreecnt=0 timerslen=0
  M4: p=1 curg=1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M3: p=-1 curg=18 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M2: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M1: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=2 dying=0 spinning=false blocked=false lockedg=-1
  M0: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  G1: status=2(preempted) m=4 lockedm=-1
  G2: status=4(force gc (idle)) m=-1 lockedm=-1
  G3: status=4(GC sweep wait) m=-1 lockedm=-1
  G4: status=4(GC scavenge wait) m=-1 lockedm=-1
  G17: status=4(finalizer wait) m=-1 lockedm=-1
  G18: status=3(GC assist marking) m=3 lockedm=-1
  G5: status=4(GC worker (idle)) m=-1 lockedm=-1
  G6: status=4(GC worker (idle)) m=-1 lockedm=-1

从调度结果上看,无论是等待gc还是正常的协程调度,都不会阻塞程序的运行,这就是基于信号的异步调度方案。

源码分析

基于1.16的代码,我们着重查看一下sysmon的执行过程。

func sysmon() {
	lock(&sched.lock)
	sched.nmsys++
	checkdead()
	unlock(&sched.lock)

	// For syscall_runtime_doAllThreadsSyscall, sysmon is
	// sufficiently up to participate in fixups.
	atomic.Store(&sched.sysmonStarting, 0)

	lasttrace := int64(0)
	idle := 0 // how many cycles in succession we had not wokeup somebody
	delay := uint32(0)

	for {
		if idle == 0 { // start with 20us sleep...
			delay = 20
		} else if idle > 50 { // start doubling the sleep after 1ms...
			delay *= 2
		}
		if delay > 10*1000 { // up to 10ms
			delay = 10 * 1000
		}
		usleep(delay)
		mDoFixup()

		// sysmon should not enter deep sleep if schedtrace is enabled so that
		// it can print that information at the right time.
		//
		// It should also not enter deep sleep if there are any active P's so
		// that it can retake P's from syscalls, preempt long running G's, and
		// poll the network if all P's are busy for long stretches.
		//
		// It should wakeup from deep sleep if any P's become active either due
		// to exiting a syscall or waking up due to a timer expiring so that it
		// can resume performing those duties. If it wakes from a syscall it
		// resets idle and delay as a bet that since it had retaken a P from a
		// syscall before, it may need to do it again shortly after the
		// application starts work again. It does not reset idle when waking
		// from a timer to avoid adding system load to applications that spend
		// most of their time sleeping.
		now := nanotime()
		if debug.schedtrace <= 0 && (sched.gcwaiting != 0 || atomic.Load(&sched.npidle) == uint32(gomaxprocs)) {
			lock(&sched.lock)
			if atomic.Load(&sched.gcwaiting) != 0 || atomic.Load(&sched.npidle) == uint32(gomaxprocs) {
				syscallWake := false
				next, _ := timeSleepUntil()
				if next > now {
					atomic.Store(&sched.sysmonwait, 1)
					unlock(&sched.lock)
					// Make wake-up period small enough
					// for the sampling to be correct.
					sleep := forcegcperiod / 2
					if next-now < sleep {
						sleep = next - now
					}
					shouldRelax := sleep >= osRelaxMinNS
					if shouldRelax {
						osRelax(true)
					}
					syscallWake = notetsleep(&sched.sysmonnote, sleep)
					mDoFixup()
					if shouldRelax {
						osRelax(false)
					}
					lock(&sched.lock)
					atomic.Store(&sched.sysmonwait, 0)
					noteclear(&sched.sysmonnote)
				}
				if syscallWake {
					idle = 0
					delay = 20
				}
			}
			unlock(&sched.lock)
		}

		lock(&sched.sysmonlock)
		// Update now in case we blocked on sysmonnote or spent a long time
		// blocked on schedlock or sysmonlock above.
		now = nanotime()

		// trigger libc interceptors if needed
		if *cgo_yield != nil {
			asmcgocall(*cgo_yield, nil)
		}
		// poll network if not polled for more than 10ms
		lastpoll := int64(atomic.Load64(&sched.lastpoll))
		if netpollinited() && lastpoll != 0 && lastpoll+10*1000*1000 < now {
			atomic.Cas64(&sched.lastpoll, uint64(lastpoll), uint64(now))  // 检查事件是否就绪
			list := netpoll(0) // non-blocking - returns list of goroutines
			if !list.empty() {
				// Need to decrement number of idle locked M's
				// (pretending that one more is running) before injectglist.
				// Otherwise it can lead to the following situation:
				// injectglist grabs all P's but before it starts M's to run the P's,
				// another M returns from syscall, finishes running its G,
				// observes that there is no work to do and no other running M's
				// and reports deadlock.
				incidlelocked(-1)
				injectglist(&list)
				incidlelocked(1)
			}
		}
		mDoFixup()
		if GOOS == "netbsd" {
			// netpoll is responsible for waiting for timer
			// expiration, so we typically don't have to worry
			// about starting an M to service timers. (Note that
			// sleep for timeSleepUntil above simply ensures sysmon
			// starts running again when that timer expiration may
			// cause Go code to run again).
			//
			// However, netbsd has a kernel bug that sometimes
			// misses netpollBreak wake-ups, which can lead to
			// unbounded delays servicing timers. If we detect this
			// overrun, then startm to get something to handle the
			// timer.
			//
			// See issue 42515 and
			// https://gnats.netbsd.org/cgi-bin/query-pr-single.pl?number=50094.
			if next, _ := timeSleepUntil(); next < now {
				startm(nil, false)
			}
		}
		if atomic.Load(&scavenge.sysmonWake) != 0 {
			// Kick the scavenger awake if someone requested it.
			wakeScavenger()
		}
		// retake P's blocked in syscalls
		// and preempt long running G's
		if retake(now) != 0 {   // 检查阻塞的系统调用和运行时间较长的协程
			idle = 0
		} else {
			idle++
		}
		// check if we need to force a GC
		if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != 0 {   // 是否需要强制GC
			lock(&forcegc.lock)
			forcegc.idle = 0
			var list gList
			list.push(forcegc.g)
			injectglist(&list)
			unlock(&forcegc.lock)
		}
		if debug.schedtrace > 0 && lasttrace+int64(debug.schedtrace)*1000000 <= now {
			lasttrace = now
			schedtrace(debug.scheddetail > 0)  // 这就是本示例打印sched的详细信息
		}
		unlock(&sched.sysmonlock)
	}
}

继续深入retake函数的流程。

// forcePreemptNS is the time slice given to a G before it is
// preempted.
const forcePreemptNS = 10 * 1000 * 1000 // 10ms

func retake(now int64) uint32 {
	...
	// We can't use a range loop over allp because we may
	// temporarily drop the allpLock. Hence, we need to re-fetch
	// allp each time around the loop.
	for i := 0; i < len(allp); i++ {
		_p_ := allp[i]
		if _p_ == nil {
			// This can happen if procresize has grown
			// allp but not yet created new Ps.
			continue
		}
		pd := &_p_.sysmontick
		s := _p_.status
		sysretake := false
		if s == _Prunning || s == _Psyscall {
			// Preempt G if it's running for too long.
			t := int64(_p_.schedtick)
			if int64(pd.schedtick) != t {
				pd.schedtick = uint32(t)
				pd.schedwhen = now
			} else if pd.schedwhen+forcePreemptNS <= now {
				preemptone(_p_)  // 检查协程是否运行了最长10ms 如果达到了则发送信号强制抢占
				// In case of syscall, preemptone() doesn't
				// work, because there is no M wired to P.
				sysretake = true
			}
		}
		...
}

通过preemptone来进行信号的发送来强制将该g进行调度。

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

	// Every call in a go routine checks for stack overflow by
	// comparing the current stack pointer to gp->stackguard0.
	// Setting gp->stackguard0 to StackPreempt folds
	// preemption into the normal stack overflow check.
	gp.stackguard0 = stackPreempt

	// Request an async preemption of this P.
	if preemptMSupported && debug.asyncpreemptoff == 0 {
		_p_.preempt = true
		preemptM(mp)
	}

	return true
}

func preemptM(mp *m) {
	// On Darwin, don't try to preempt threads during exec.
	// Issue #41702.
	if GOOS == "darwin" || GOOS == "ios" {
		execLock.rlock()
	}

	if atomic.Cas(&mp.signalPending, 0, 1) {
		if GOOS == "darwin" || GOOS == "ios" {
			atomic.Xadd(&pendingPreemptSignals, 1)
		}

		// If multiple threads are preempting the same M, it may send many
		// signals to the same M such that it hardly make progress, causing
		// live-lock problem. Apparently this could happen on darwin. See
		// issue #37741.
		// Only send a signal if there isn't already one pending.
		signalM(mp, sigPreempt)   // 发送强制信号
	}

	if GOOS == "darwin" || GOOS == "ios" {
		execLock.runlock()
	}
}

...
func signalM(mp *m, sig int) {
	pthread_kill(pthread(mp.procid), uint32(sig))
}

至此可以清晰的看出通过pthread_kill来想线程发送信号,从而完成线程通知而完成强制调度,此时在go启动的过程中就注册了信号处理函数,在完成一系列安全等的检查之后就会调用schedule()函数。

c语言的实现

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

/* Simple error handling functions */

static void *
sig_thread(void *arg)
{
   sigset_t *set = arg;
   int s, sig;
   for (;;) {
       printf("thread(%lu) i: %d\n", pthread_self(), 1);
        sleep(60);
       printf("next (%lu) i: %d\n", pthread_self(), 1);
   }
}



//信号的异步处理方式
void sig_handler(int signo)
{
    printf("thread(%lu) call sig_handler: signal(%d)\n", pthread_self(), signo);
}

//信号的同步处理
void sigwait_handler(int signo)
{
    printf("sub thread(%lu) call sigwait_handler: signal(%d)\n",pthread_self(), signo);
}


int
main(int argc, char *argv[])
{
   int NUMTHREADS = 5;
   pthread_t threads[NUMTHREADS];
   sigset_t set;
   int s;

    printf("main thread(%lu) \n",pthread_self());
   //注册异步信号处理函数
    if(signal(SIGUSR1, sig_handler)== SIG_ERR){
        perror("signal sigusr1 error");
    }
    if(signal(SIGTSTP, sig_handler)==SIG_ERR){
        perror("signal sigtstp error");
    }

   int i = 0;
   for(i=0;i<NUMTHREADS;i++) {
        pthread_create(&threads[i], NULL, &sig_thread, NULL);
   }

   for(;;){
      int i=0;
      for(i=0;i<NUMTHREADS;i++){
        pthread_kill(threads[i],SIGUSR1);
        sleep(10);
      }
   }

   /* Main thread carries on to create other threads and/or do
      other work. */

   printf("Hello, World!");
   sleep(10000);
}

此时,我们编译运行一下,就能从终端中得到如下信息。

main thread(140400981489472)
thread(140400973096704) i: 1
thread(140400964704000) i: 1
thread(140400939525888) i: 1
thread(140400956311296) i: 1
thread(140400973096704) call sig_handler: signal(10)
next (140400973096704) i: 1
thread(140400973096704) i: 1
thread(140400947918592) i: 1
thread(140400964704000) call sig_handler: signal(10)
next (140400964704000) i: 1
thread(140400964704000) i: 1
thread(140400956311296) call sig_handler: signal(10)
next (140400956311296) i: 1
thread(140400956311296) i: 1
thread(140400947918592) call sig_handler: signal(10)
next (140400947918592) i: 1
thread(140400947918592) i: 1

从输出来看,每个线程都调用了主线程注册的回调函数来进行处理。go的异步协程调度也基于此原理。

总结

本文主要是简单概要的尝试了一下go1.14之后新加的信号异步协程调度方案,从使用的效果上来看,这种方案确实增强了在某些情况下协程长时间运行的问题,这也使得用户写的程序的健壮性会更强。