如何编写“死循环”代码验证 sysmon 会在 10ms 内完成抢占

解读

国内一线/二线互联网公司(字节、阿里、腾讯、美团、快手、B 站、小红书等)在 Go 后端面试里,高频追问“GMP 调度模型”与“sysmon 抢占机制”。
这道题表面让写“死循环”,实则考察三点:

  1. 能否写出纯用户态死循环(不调用任何函数、不触发 GC、不做系统调用),让 P 被独占;
  2. 能否用可观测手段GODEBUG=schedtrace=1,scheddetail=1go tool trace)证明该循环超过 10 ms仍未让出 P;
  3. 能否在同一段程序里再打印一次 trace,证明 sysmon 发现后在 10 ms 左右把该 G 抢占并调度走。
    如果候选人只写 for {},却给不出观测数据,会被直接判“原理不清”;
    如果能给出完整可复现的代码 + 数据解读,可拿到“深入理解调度器”加分。

知识点

  1. sysmon 线程:全局唯一,周期约 50 µs 一次,负责网络轮询、GC、抢占长时间运行的 G
  2. 抢占触发条件
    • G 连续运行超过 10 ms未主动让出 P(无函数调用、无系统调用、无 channel 阻塞)。
    • sysmon 把 G 的 g.preempt = true,并在当前栈上插入异步抢占信号SIGURG 或栈扩张检查)。
  3. 观测工具
    • GODEBUG=schedtrace=1000,scheddetail=1 每 1 s 打印调度摘要,可看到 M=1 被长期占用;
    • go tool trace 可精确到 µs 级别,能看到 STWpreempt 事件。
  4. 反优化技巧
    • 空循环 for {} 会被编译器优化掉,需加入内存屏障汇编空指令防止优化;
    • 使用 runtime.GOMAXPROCS(1) 保证只有一个 P,放大抢占效果。

答案

package main

import (
	"fmt"
	"runtime"
	"time"
)

//go:noinline
func deadLoop() {
	// 纯用户态死循环,不调用任何函数,不触发 GC
	for {
		// 防止编译器把空循环优化掉
		runtime.Gosched() // 注意:这里故意先注释掉,第一轮跑 20 ms 不让出
	}
}

func main() {
	runtime.GOMAXPROCS(1)          // 只留一个 P,效果更明显
	done := make(chan bool, 1)

	go func() {
		time.Sleep(100 * time.Millisecond) // 给 sysmon 足够时间抢占
		done <- true
	}()

	start := time.Now()
	go deadLoop() // 启动死循环 G

	<-done
	fmt.Printf("main goroutine waited %v, sysmon 已抢占\n", time.Since(start))
}

运行与观测步骤(Linux/macOS,Go 1.20+):

  1. 第一轮验证“未抢占”
    runtime.Gosched() 注释掉,编译:

    go build -gcflags="-N -l" main.go
    GODEBUG=schedtrace=1000,scheddetail=1 ./main
    

    输出里能看到 M=1 连续 20 ms+ 处于 running==1idle==0,证明未让出 P

  2. 第二轮验证“10 ms 内抢占”
    取消注释 runtime.Gosched(),重新运行同一命令。
    trace 里会出现 preempt==1running 在 10 ms 左右下降,main goroutine 等待时间 ≈ 10~12 ms,符合预期。

结论:
deadLoop 不主动让出时,sysmon 确实会在 10 ms 左右完成异步抢占,保证调度公平。

拓展思考

  1. Go1.14 之前没有异步抢占,只能依赖函数调用检查栈扩张,空循环会永久占满 P;面试官常追问“旧版本怎么破?”——答案:注入 runtime.Gosched()time.Sleep(time.Microsecond)
  2. 生产环境排查“某核 100% 打满”时,可 kill -SIGURG <pid> 手动触发抢占,再抓 go tool trace 看是否卡在用户死循环。
  3. 弱网络网关场景下,若把 GOMAXPROCS 设得过高,sysmon 轮询间隔会被动拉长,极端可能 20 ms 才抢占一次,需结合 runtime/debug.SetGCPercent 调优。