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.profmem.prof 中获得任何输出。

注意:我知道如何修复这个问题。这不是我在这里要问的。我问的是为什么Go运行时没有报告死锁。


更多关于Golang程序意外挂起而非显示"all goroutines are asleep"的问题排查的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

非常感谢您的分享。这帮助我解决了我的问题。

更多关于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

这让我感到非常着迷。我能够用这个重现问题,但是你必须下载并在本地运行它!在Playground中它不会发生死锁!

我仍然不清楚发生了什么,但我注意到的一个区别是,当我在本地运行此程序时,对examiner.Supervise的调用有3个参数:[]*q.Q切片的数据、长度和容量:

gitlab.com/aryzing/repro-go-deadlock/examiner.Supervise(0xc420010c00, 0x2, 0x2)

但是,在我于Playground上创建的小型可重现场景的堆栈跟踪中,出现了第4个参数:

main.Supervise(0x85efa4, 0x3, 0x3, 0x1b10)

我不确定原因,或者这是否有任何关联。

仍在调查中…

这是一个典型的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()  // 这里也会永久阻塞
}

关键问题

  1. 主goroutine在第一次mu.Lock()后没有解锁
  2. 两个goroutine都阻塞在sync.Mutex.Lock()
  3. Go的死锁检测器不检测阻塞在sync.Mutex上的死锁

Go运行时死锁检测的局限性

  • 只检测所有goroutine都阻塞在channel操作的情况
  • 不检测阻塞在sync.Mutexsync.WaitGroupsync.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()  // 程序挂起,无错误信息
}

调试建议

  1. 使用go run -race检测数据竞争
  2. 使用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堆栈

  1. 使用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的阻塞状态。

回到顶部