Golang程序意外挂起而非显示"all goroutines are asleep"的问题排查
Golang程序意外挂起而非显示"all goroutines are asleep"的问题排查
我不小心创建了一个我认为是死锁的情况,原本期望会出现“所有goroutine都处于休眠状态”的错误,但程序却只是无限期地挂起。我原以为可能是因为还有另一个goroutine在运行,但似乎情况并非如此(在预期的死锁发生前,runtime.NumGoroutine() 打印出 1)。
我非常希望能得到一些帮助来调试这个应用程序,一个非常精简的复现版本在这个仓库中:https://gitlab.com/aryzing/repro-go-deadlock
我已经尝试过的其他方法包括:
- 确保没有任何依赖项启动了可能在后台运行的goroutine,playground
- 在上述仓库的
with-profiling分支中设置了pprof,但无法在cpu.prof或mem.prof中获得任何输出。
注意:我知道如何修复这个问题。这不是我在这里要问的。我问的是为什么Go运行时没有报告死锁。
更多关于Golang程序意外挂起而非显示"all goroutines are asleep"的问题排查的实战教程也可以访问 https://www.itying.com/category-94-b0.html
非常感谢您的分享。这帮助我解决了我的问题。
更多关于Golang程序意外挂起而非显示"all goroutines are asleep"的问题排查的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
好发现!也许这应该被更好地记录下来 🙂 感谢所有的帮助!
这似乎来自一个旧版本的Go,但它可能仍然相关:https://github.com/golang/go/issues/12734
好的,如果我将 _ "net/http/pprof" 注释掉,死锁就会被识别出来。
examiner.InitExaminer() 难道不应该在 examiner.Supervise 之前运行吗?否则就没有任何东西从通道读取了?但这应该会导致死锁。
感谢您的深入分析!我不太确定注释掉那个导入是如何帮助识别死锁错误缺失的。在 master 分支上,没有使用 pprof,并且应用程序从未出错退出。在 with-profiling 分支上,即使在注释掉 "net/http/pprof" 之后,应用程序也没有报告死锁错误。
我使用的环境是 go version go1.12.7 linux/amd64
这是一个典型的Go运行时死锁检测边界情况。你的程序挂起而非报告死锁,是因为Go的死锁检测器只会在所有goroutine都阻塞在channel操作或同步原语时触发,但你的代码中存在一个goroutine阻塞在非channel的同步操作上。
让我分析你的示例代码:
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
var mu sync.Mutex
// 锁定互斥锁
mu.Lock()
// 启动一个goroutine尝试获取已锁定的互斥锁
go func() {
mu.Lock() // 这里会永久阻塞
fmt.Println("This never prints")
}()
// 主goroutine也尝试获取同一个互斥锁
fmt.Printf("Number of goroutines: %d\n", runtime.NumGoroutine())
mu.Lock() // 这里也会永久阻塞
}
关键问题:
- 主goroutine在第一次
mu.Lock()后没有解锁 - 两个goroutine都阻塞在
sync.Mutex.Lock()上 - Go的死锁检测器不检测阻塞在
sync.Mutex上的死锁
Go运行时死锁检测的局限性:
- 只检测所有goroutine都阻塞在channel操作的情况
- 不检测阻塞在
sync.Mutex、sync.WaitGroup、sync.Cond等同步原语上的死锁 - 不检测阻塞在系统调用、CGO、I/O操作上的情况
验证示例:
package main
func main() {
ch := make(chan int)
// 情况1:阻塞在channel上 - 会报告死锁
// <-ch // fatal error: all goroutines are asleep - deadlock!
// 情况2:阻塞在mutex上 - 不会报告死锁
var mu sync.Mutex
mu.Lock()
mu.Lock() // 程序挂起,无错误信息
}
调试建议:
- 使用
go run -race检测数据竞争 - 使用pprof检查goroutine状态:
import (
"net/http"
_ "net/http/pprof"
)
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// ... 你的代码
}
然后访问http://localhost:6060/debug/pprof/goroutine?debug=2查看所有goroutine堆栈
- 使用
runtime.Stack获取所有goroutine信息:
buf := make([]byte, 1024*1024)
n := runtime.Stack(buf, true)
fmt.Printf("%s\n", buf[:n])
根本原因:Go的死锁检测器设计上只处理channel阻塞,因为channel是Go并发模型的核心原语。其他同步原语的死锁需要开发者自己保证或使用工具检测。
这就是为什么你的程序挂起而没有显示"all goroutines are asleep"的原因——死锁检测器根本没有检测sync.Mutex的阻塞状态。


