Go运行时死锁检测无性能开销,仅在所有goroutine均阻塞且无可运行任务时触发一次panic。它不采样、不循环、不埋点,真正开销来自未被检测到的长期阻塞操作。

Go 运行时的死锁检测本身没有性能开销——它只在程序“疑似完全卡死”时才触发一次性检查,不参与日常调度。 你看到的 fatal error: all goroutines are asleep - deadlock! 不是监控系统持续采样得出的结论,而是 panic 前的最后一道兜底判断。
死锁检测何时发生?
运行时仅在满足全部以下条件时才判定死锁并 panic:
- 所有 M(OS 线程)都空闲,且无网络轮询器活动(
netpoll无待处理事件) - 所有现存 goroutine 都处于 channel 操作阻塞态(如
<-ch、ch <- x)或休眠态(runtime.Gosched、time.Sleep) - 没有正在执行的 goroutine,也没有待运行的 goroutine(
g.runqhead == nil且g.m.p.runq为空)
换句话说:它不是“检测”,而是“确认无人可动”。这个检查发生在 scheduler 的主循环末尾,属于极低频、零周期开销的操作。
真正有开销的是你误以为“没死锁”却长期阻塞的场景
比如一个 goroutine 卡在 <-ch 上等 5 秒,其他 goroutine 正常运行——这不会触发死锁 panic,但会拖慢业务。这类问题的开销来自:
立即学习“go语言免费学习笔记(深入)”;
- goroutine 长期阻塞导致
runtime.NumGoroutine()持续偏高,增加调度器扫描成本 - pprof /debug/pprof/goroutine?debug=2 抓堆栈时需遍历所有 goroutine,阻塞 goroutine 的 stack trace 更深、更难裁剪
- 若使用
GODEBUG=schedtrace=1000,每秒输出调度器快照,阻塞 goroutine 会反复出现在goroutines: N统计中,干扰人工判断
怎么验证死锁检测本身没开销?
你可以用两个对照实验确认:
- 启动一个纯空闲程序:
func main() { select {} },用perf record -e cycles,instructions go run .观察,没有任何死锁检测相关函数(如dumpgstatus、deadlock)被调用 - 触发真实死锁后看 panic 堆栈,最后一帧一定是
runtime.checkdeadlock—— 它只在 fatal 时刻执行一次,不循环、不采样、不埋点
所以别优化“死锁检测”,要盯住那些没被 runtime 发现、但实际已卡住的 channel 操作和锁等待。
容易被忽略的关键点
很多人把数据库死锁(如 MySQL 1213)、context 超时、HTTP client hang、甚至磁盘 I/O 卡顿,都误判为 Go 运行时死锁。它们共享表象(goroutine 堆栈停在某处),但 checkdeadlock 根本不会介入——因为那些 goroutine 其实正 block 在系统调用里(SYS_read、SYS_epoll_wait),不属于“asleep”状态。真要定位,得靠 strace 或 bpftrace,而不是盯着 pprof/goroutine。



















