前言
最近提交了一份死锁代码,导致某个功能不可用,前端小哥纳闷反馈,这昨天还能用的,今天怎么就不行了?
再一看原来是死锁了
问题
- 代码没有做自测,认为是很简单的修改,不会出
bug
,大锅。 - git 提交后的 CI 没有做死锁的检查。
官方是否提供了死锁检测呢?
解决
上网搜了一圈,发现官网没有死锁检测,接下来介绍今天的主角:https://github.com/sasha-s/go-deadlock,这是基于运行时的代码检测的。
体现在代码中,就是用 deadlock
的同步原语来代替标准库 sync
的同步原语。
先来看一个简单使用的例子:
// 测试 deadlock 的锁检测超时时间
// 同一把锁在不同的协程中去获取
func tGoDeadlock1() {
start := time.Now()
defer func() {
fmt.Println("耗时:", time.Now().Sub(start))
}()
var wg sync.WaitGroup
wg.Add(1)
deadlock.Opts.DeadlockTimeout = time.Second // 获取锁超时控制
//deadlock.Opts.Disable = true // 生产环境可以将其设置为 true
var mu deadlock.Mutex
mu.Lock()
go func() {
mu.Lock()
defer mu.Unlock()
fmt.Println("i'm in goroutine")
wg.Done()
}()
time.Sleep(time.Second * 15)
mu.Unlock()
wg.Wait()
}
两个协程同时获取同一把锁,我们都知道这时会死锁,导致进程不退出,但是上述代码输出结果如下:
我们可以看到代码直接 panic
了,并且告知了 panic
是因为尝试上锁的时间超过了 1s
。我在这里添加了deadlock.Opts.DeadlockTimeout = time.Second
,设置了获取锁超过检测为 1s
。
源码分析
打开源码发现其原理并没有很复杂,简单来说就是在上锁的时候会触发 lock
方法,检测是否有冲突的锁关系。通过 lockOrder
存储了已经上锁的内存信息,结构体如下:
type lockOrder struct {
mu sync.Mutex
cur map[interface{}]stackGID // stacktraces + gids for the locks currently taken.
order map[beforeAfter]ss // expected order of locks.
}
type stackGID struct {
stack []uintptr
gid int64
}
type beforeAfter struct {
before interface{}
after interface{}
}
type ss struct {
before []uintptr
after []uintptr
}
下面再贴一小部分源码,看看其具体的过程。
获取锁:
func (m *Mutex) Lock() {
lock(m.mu.Lock, m)
}
func lock(lockFn func(), ptr interface{}) {
if Opts.Disable {
lockFn()
return
}
stack := callers(1)
preLock(stack, ptr)
if Opts.DeadlockTimeout <= 0 {
lockFn()
} else {
ch := make(chan struct{})
currentID := goid.Get()
go func() {
for {
t := time.NewTimer(Opts.DeadlockTimeout)
defer t.Stop() // This runs after the losure finishes, but it's OK.
select {
case <-t.C:
lo.mu.Lock()
prev, ok := lo.cur[ptr]
if !ok {
lo.mu.Unlock()
break // Nobody seems to be holding the lock, try again.
}
Opts.mu.Lock()
fmt.Fprintln(Opts.LogBuf, header)
fmt.Fprintln(Opts.LogBuf, "Previous place where the lock was grabbed")
fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", prev.gid, ptr)
printStack(Opts.LogBuf, prev.stack)
fmt.Fprintln(Opts.LogBuf, "Have been trying to lock it again for more than", Opts.DeadlockTimeout)
fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", currentID, ptr)
printStack(Opts.LogBuf, stack)
stacks := stacks()
grs := bytes.Split(stacks, []byte("\n\n"))
for _, g := range grs {
if goid.ExtractGID(g) == prev.gid {
fmt.Fprintln(Opts.LogBuf, "Here is what goroutine", prev.gid, "doing now")
Opts.LogBuf.Write(g)
fmt.Fprintln(Opts.LogBuf)
}
}
lo.other(ptr)
if Opts.PrintAllCurrentGoroutines {
fmt.Fprintln(Opts.LogBuf, "All current goroutines:")
Opts.LogBuf.Write(stacks)
}
fmt.Fprintln(Opts.LogBuf)
if buf, ok := Opts.LogBuf.(*bufio.Writer); ok {
buf.Flush()
}
Opts.mu.Unlock()
lo.mu.Unlock()
Opts.OnPotentialDeadlock()
<-ch
return
case <-ch:
return
}
}
}()
lockFn()
postLock(stack, ptr)
close(ch)
return
}
postLock(stack, ptr)
}
func preLock(stack []uintptr, p interface{}) {
lo.preLock(stack, p)
}
func (l *lockOrder) preLock(stack []uintptr, p interface{}) {
if Opts.DisableLockOrderDetection {
return
}
gid := goid.Get()
l.mu.Lock()
for b, bs := range l.cur {
if b == p {
if bs.gid == gid {
Opts.mu.Lock()
fmt.Fprintln(Opts.LogBuf, header, "Recursive locking:")
fmt.Fprintf(Opts.LogBuf, "current goroutine %d lock %p\n", gid, b)
printStack(Opts.LogBuf, stack)
fmt.Fprintln(Opts.LogBuf, "Previous place where the lock was grabbed (same goroutine)")
printStack(Opts.LogBuf, bs.stack)
l.other(p)
if buf, ok := Opts.LogBuf.(*bufio.Writer); ok {
buf.Flush()
}
Opts.mu.Unlock()
Opts.OnPotentialDeadlock()
}
continue
}
if bs.gid != gid { // We want locks taken in the same goroutine only.
continue
}
if s, ok := l.order[beforeAfter{p, b}]; ok {
Opts.mu.Lock()
fmt.Fprintln(Opts.LogBuf, header, "Inconsistent locking. saw this ordering in one goroutine:")
fmt.Fprintln(Opts.LogBuf, "happened before")
printStack(Opts.LogBuf, s.before)
fmt.Fprintln(Opts.LogBuf, "happened after")
printStack(Opts.LogBuf, s.after)
fmt.Fprintln(Opts.LogBuf, "in another goroutine: happened before")
printStack(Opts.LogBuf, bs.stack)
fmt.Fprintln(Opts.LogBuf, "happened after")
printStack(Opts.LogBuf, stack)
l.other(p)
fmt.Fprintln(Opts.LogBuf)
if buf, ok := Opts.LogBuf.(*bufio.Writer); ok {
buf.Flush()
}
Opts.mu.Unlock()
Opts.OnPotentialDeadlock()
}
l.order[beforeAfter{b, p}] = ss{bs.stack, stack}
if len(l.order) == Opts.MaxMapSize { // Reset the map to keep memory footprint bounded.
l.order = map[beforeAfter]ss{}
}
}
l.mu.Unlock()
}
释放锁:
func (m *Mutex) Unlock() {
m.mu.Unlock()
if !Opts.Disable {
postUnlock(m)
}
}
func postUnlock(p interface{}) {
lo.postUnlock(p)
}
func (l *lockOrder) postUnlock(p interface{}) {
l.mu.Lock()
delete(l.cur, p)
l.mu.Unlock()
}
总结
go-deadlock 封装了官方标准库的 Mutex
, Cond
, Once
, Pool
, WaitGroup
等,为 Lock
方法添加了一层判断死锁的逻辑,源码也不是很多,可以自行学习。
注意:最好不要将 deadlock
直接用在生产环境中,记得设置 deadlock.Opts.Disable = true
。
相关代码在我的 Github/GoTest 仓库 ,syncTest
目录下的 go-deadlock.go
文件中,这个项目主要是日常学习的一些记录与测试,感兴趣的可以看看。