sasha-s/go-deadlock
GitHub: sasha-s/go-deadlock
Go 语言的在线死锁检测库,通过替代标准库互斥锁实现在运行时自动发现并报告不一致的加锁顺序及长时间锁等待问题。
Stars: 1192 | Forks: 79
# 在 go (golang) 中进行在线死锁检测。 [](https://wandbox.org/permlink/hJc6QCZowxbNm9WW) [](https://godoc.org/github.com/sasha-s/go-deadlock) [](https://codecov.io/gh/sasha-s/go-deadlock) [](https://github.com/sasha-s/go-deadlock/releases) [](https://goreportcard.com/report/github.com/sasha-s/go-deadlock) [](https://opensource.org/licenses/Apache-2.0)
## 为什么
死锁时有发生,并且调试起来非常痛苦。
## 是什么
go-deadlock 提供了 sync.(RW)Mutex 的 (RW)Mutex 直接替代品。
如果你创建了像意大利面条一样错综复杂的 channel,它将无法工作。
仅支持 Mutex。
## 安装说明
```
go get github.com/sasha-s/go-deadlock/...
```
## 用法
```
import "github.com/sasha-s/go-deadlock"
var mu deadlock.Mutex
// Use normally, it works exactly like sync.Mutex does.
mu.Lock()
defer mu.Unlock()
// Or
var rw deadlock.RWMutex
rw.RLock()
defer rw.RUnlock()
```
### 死锁
最常见的死锁来源之一是不一致的加锁顺序:
假设你有两个 mutex A 和 B,在某些 goroutine 中你执行了
```
A.Lock() // defer A.Unlock() or similar.
...
B.Lock() // defer B.Unlock() or similar.
```
而在另一个 goroutine 中,加锁的顺序是相反的:
```
B.Lock() // defer B.Unlock() or similar.
...
A.Lock() // defer A.Unlock() or similar.
```
另一种常见的死锁来源是在同一个 goroutine 中重复获取锁:
```
A.Rlock() or lock()
A.lock() or A.RLock()
```
这并不一定会导致死锁(也许上面的 goroutine 永远不会同时运行),但这至少通常是一个设计缺陷。
go-deadlock 可以检测到此类情况(除非你跨越了 goroutine 边界 —— 比如锁定 A,然后生成一个 goroutine,阻塞直到它发出信号,并在该 goroutine 内部锁定 B),即使死锁本身发生的频率极低且难以复现!
每次 go-deadlock 遇到对锁 B 的加锁尝试时,它都会记录下在同一个 goroutine 中当前持有的每个锁的“A 先于 B”的顺序,并且当它发现加锁顺序被违反时,它会打印出来(默认情况下会退出程序)。
此外,如果它发现我们正在等待一把锁很长时间(opts.DeadlockTimeout,默认为 30 秒),它会报告一个潜在死锁,同时打印出当前持有我们急需获取的锁的那个 goroutine 的 stacktrace。
## 示例输出
#### 不一致的加锁顺序:
```
POTENTIAL DEADLOCK: Inconsistent locking. saw this ordering in one goroutine:
happened before
inmem.go:623 bttest.(*server).ReadModifyWriteRow { r.mu.Lock() } <<<<<
inmem_test.go:118 bttest.TestConcurrentMutationsReadModifyAndGC.func4 { _, _ = s.ReadModifyWriteRow(ctx, rmw()) }
happened after
inmem.go:629 bttest.(*server).ReadModifyWriteRow { tbl.mu.RLock() } <<<<<
inmem_test.go:118 bttest.TestConcurrentMutationsReadModifyAndGC.func4 { _, _ = s.ReadModifyWriteRow(ctx, rmw()) }
in another goroutine: happened before
inmem.go:799 bttest.(*table).gc { t.mu.RLock() } <<<<<
inmem_test.go:125 bttest.TestConcurrentMutationsReadModifyAndGC.func5 { tbl.gc() }
happend after
inmem.go:814 bttest.(*table).gc { r.mu.Lock() } <<<<<
inmem_test.go:125 bttest.TestConcurrentMutationsReadModifyAndGC.func5 { tbl.gc() }
```
#### 长时间等待锁:
```
POTENTIAL DEADLOCK:
Previous place where the lock was grabbed
goroutine 240 lock 0xc820160440
inmem.go:799 bttest.(*table).gc { t.mu.RLock() } <<<<<
inmem_test.go:125 bttest.TestConcurrentMutationsReadModifyAndGC.func5 { tbl.gc() }
Have been trying to lock it again for more than 40ms
goroutine 68 lock 0xc820160440
inmem.go:785 bttest.(*table).mutableRow { t.mu.Lock() } <<<<<
inmem.go:428 bttest.(*server).MutateRow { r := tbl.mutableRow(string(req.RowKey)) }
inmem_test.go:111 bttest.TestConcurrentMutationsReadModifyAndGC.func3 { s.MutateRow(ctx, req) }
Here is what goroutine 240 doing now
goroutine 240 [select]:
github.com/sasha-s/go-deadlock.lock(0xc82028ca10, 0x5189e0, 0xc82013a9b0)
/Users/sasha/go/src/github.com/sasha-s/go-deadlock/deadlock.go:163 +0x1640
github.com/sasha-s/go-deadlock.(*Mutex).Lock(0xc82013a9b0)
/Users/sasha/go/src/github.com/sasha-s/go-deadlock/deadlock.go:54 +0x86
google.golang.org/cloud/bigtable/bttest.(*table).gc(0xc820160440)
/Users/sasha/go/src/google.golang.org/cloud/bigtable/bttest/inmem.go:814 +0x28d
google.golang.org/cloud/bigtable/bttest.TestConcurrentMutationsReadModifyAndGC.func5(0xc82015c760, 0xc820160440) /Users/sasha/go/src/google.golang.org/cloud/bigtable/bttest/inmem_test.go:125 +0x48
created by google.golang.org/cloud/bigtable/bttest.TestConcurrentMutationsReadModifyAndGC
/Users/sasha/go/src/google.golang.org/cloud/bigtable/bttest/inmem_test.go:126 +0xb6f
```
## 使用场景
[cockroachdb:Gossip.SetStorage 和 Node.gossipStores 之间的潜在死锁](https://github.com/cockroachdb/cockroach/issues/7972)
[bigtable/bttest:GC 与行修改之间的竞态](https://code-review.googlesource.com#/c/5301/)
## 需要一个能配合 net.context 工作的 mutex?
我有一个[在这里](https://github.com/sasha-s/go-csync)。
## 从同一个 goroutine 两次获取 RLock
令人惊讶的是,这并不是一个好主意!
根据 [RWMutex](https://golang.org/pkg/sync/#RWMutex) 文档:
以下代码将发生死锁 — [在 playground 上运行此示例](https://play.golang.org/p/AkL-W63nq5f) 或[在 wandbox 上使用 go-deadlock 在线尝试](https://wandbox.org/permlink/JwnL0GMySBju4SII):
```
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.RWMutex
chrlockTwice := make(chan struct{}) // Used to control rlockTwice
rlockTwice := func() {
mu.RLock()
fmt.Println("first Rlock succeeded")
<-chrlockTwice
<-chrlockTwice
fmt.Println("trying to Rlock again")
mu.RLock()
fmt.Println("second Rlock succeeded")
mu.RUnlock()
mu.RUnlock()
}
chLock := make(chan struct{}) // Used to contol lock
lock := func() {
<-chLock
fmt.Println("about to Lock")
mu.Lock()
fmt.Println("Lock succeeded")
mu.Unlock()
<-chLock
}
control := func() {
chrlockTwice <- struct{}{}
chLock <- struct{}{}
close(chrlockTwice)
close(chLock)
}
go control()
go lock()
rlockTwice()
}
```
## 构建标签和兼容模式
go-deadlock 支持针对不同使用场景的多种构建配置:
* **普通模式**(默认):带计时器池的完整死锁检测
* **Synctest 模式**:兼容 Go 的 `testing/synctest` 包 - 可以使用以下任意一种:
* `-tags=deadlock_synctest`(推荐用于 Go 1.25+)
* `-tags=goexperiment.synctest`(用于实验性 synctest)
* **禁用模式**(`-tags=deadlock_disable`):零开销,无检测
**为什么使用 synctest 模式?** `sync.Mutex` 在 synctest 气泡中不是持久阻塞的,但 channel 可以。Synctest 模式使用基于 channel 的 mutex 来确保与 `testing/synctest` 的正确交互行为。
### 快速示例
```
# 正常开发/测试
go test ./...
# 使用 synctest 进行测试 (Go 1.25+, 推荐)
GODEBUG=asynctimerchan=0 go test -tags=deadlock_synctest ./...
# 使用实验性 synctest 进行测试
GODEBUG=asynctimerchan=0 go test -tags=goexperiment.synctest ./...
# 零开销的生产构建
go build -tags=deadlock_disable ./...
```
## 配置 go-deadlock
请查看 [Opts](https://pkg.go.dev/github.com/sasha-s/go-deadlock#pkg-variables)。
* `Opts.Disable`:完全禁用死锁检测(runtime 选项;另请参阅 `deadlock_disable` 构建标签)
* `Opts.DisableLockOrderDetection`:禁用基于锁顺序的死锁检测。
* `Opts.DeadlockTimeout`:在 mutex 上阻塞超过 DeadlockTimeout 的时间将被视为死锁。如果为负数则忽略
* `Opts.OnPotentialDeadlock`:检测到死锁时的回调函数
* `Opts.MaxMapSize`:happens before // happens after 表的大小
* `Opts.PrintAllCurrentGoroutines`:检测到不一致的加锁时,输出所有 goroutine 的 stacktrace,内容详细
* `Opts.LogBuf`:写入死锁信息/stacktrace 的位置
* `Opts.TimerPool`:控制计时器池行为(根据构建标签自动配置)
标签:EVTX分析, Golang, SOC Prime, 安全编程, 并发编程, 开发工具, 日志审计, 死锁检测