跳转到主内容
websoft网络软件专家 - 深耕网络技术,打造实用软件!

如何在不停止程序的情况下统计当前活跃的 Goroutine 数量?

runtime.NumGoroutine()返回当前已启动且尚未退出的goroutine总数,包括运行、就绪、阻塞及刚创建未调度的协程,是无锁原子快照,开销极低;它包含用户与runtime内部协程,不能区分来源,仅作数量参考。 直接调用
runtime.NumGoroutine()
就行 它返回的是当前已启动且尚未退出的 goroutine 总数,包括正在运行、就绪、阻塞(如 channel 等待、syscall、time.Sleep)、甚至刚创建还没调度的协程。这是个无锁原子读取,开销极低,调用即返回,完全不影响程序运行。 常见误判是看到数值为 7 就以为自己写了 5 个 goroutine 没关——其实其中 2–5 个大概率是 runtime 自带的 netpoll worker、timer goroutine 或 GC 辅助协程,它们长期存在且稳定。 值为
1
表示只有
main
goroutine 在跑,程序基本空闲 值持续 >1000 不一定异常,但若伴随 CPU/内存上涨,就得查泄漏 别在每请求 handler 里狂打
log.Println(runtime.NumGoroutine())
,日志 IO 才是瓶颈,不是这个函数 为什么
pprof/goroutine?debug=2
和
runtime.NumGoroutine()
数值不一致?
runtime.NumGoroutine()
是一个整数快照;而
/debug/pprof/goroutine?debug=2
输出的是所有 goroutine 的栈跟踪列表,默认会过滤掉大量 idle 状态的 runtime 内部协程(比如
runtime.gopark
在 netpoll 上休眠的那些)。 所以你看到的 pprof 行数通常比
runtime.NumGoroutine()
小,差值就是这些被隐藏的 idle worker。想尽量对齐,得用
?debug=2
,但即使如此,某些刚创建还没入调度队列的 goroutine 仍可能不出现。 pprof 的
?debug=1
只显示“正在运行或可运行”的 goroutine,摘要式统计
?debug=2
才打印全部堆栈,适合人工排查阻塞点 数值差异本身不是 bug,是视角不同:一个是总数,一个是可观察堆栈快照 想监控特定业务逻辑的 goroutine 数量怎么办?
runtime.NumGoroutine()
无法区分用户代码和 runtime 协程,更没法按函数维度拆解。真要精确统计某类任务(比如
processOrder
或
fetchFromDB
)的并发实例数,必须自己埋点。 用
sync/atomic
做原子计数是最轻量、最可靠的方式:入口
atomic.AddInt64(&counter, 1)
,出口用
defer atomic.AddInt64(&counter, -1)
,读取时用
atomic.LoadInt64(&counter)
。 计数器变量必须是包级全局
int64
,不能是局部变量 务必用
defer
,否则 panic 会导致计数永远卡住 不要用
sync.Mutex
包裹,原子操作足够,加锁反而引入竞争 生产环境暴露指标时最容易踩的坑 把
runtime.NumGoroutine()
直接喂给 Prometheus,不能图省事写成
prometheus.NewGaugeFunc(..., func() float64 { return float64(runtime.NumGoroutine()) })
—— 这会让每次 HTTP 抓取
/metrics
都触发一次实时计算,可能卡住 metrics endpoint。 正确做法是启一个后台 goroutine,每 1–5 秒调用一次
gauge.Set(float64(runtime.NumGoroutine()))
,再注册该 gauge 实例。 更新间隔别小于 1 秒:runtime 内部有读写锁,太高频反而增加争用 别漏掉
prometheus.MustRegister(gauge)
,否则 /metrics 返回 200 但没数据 如果做告警,建议用滑动窗口最大值(如最近 30 秒),避免瞬时 burst 导致误报 实际泄漏往往藏在第三方库启动却没提供关闭接口的 goroutine 里,比如某些 logger、metrics collector 或旧版 http client。数值只是线索,
/debug/pprof/goroutine?debug=2
才是定位卡点的第一现场。

相关文章