前言

最近提交了一份死锁代码,导致某个功能不可用,前端小哥纳闷反馈,这昨天还能用的,今天怎么就不行了?

再一看原来是死锁了

android检测死锁 死锁检测工具_android检测死锁

问题

  1. 代码没有做自测,认为是很简单的修改,不会出 bug,大锅。
  2. 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()
}

两个协程同时获取同一把锁,我们都知道这时会死锁,导致进程不退出,但是上述代码输出结果如下:

android检测死锁 死锁检测工具_开发语言_02


我们可以看到代码直接 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 文件中,这个项目主要是日常学习的一些记录与测试,感兴趣的可以看看。