小工具      在线工具  汉语词典  css  js  c++  java

go 中的死锁检测,第三方工具 go-deadlock

go,golang,开发语言,后端 额外说明

收录于:112天前

前言

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

再一看原来是死锁了
尴尬

问题

  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()
}

两个协程同时获取同一把锁,我们都知道这时会死锁,导致进程不退出,但是上述代码输出结果如下:
在这里插入图片描述
我们可以看到代码直接 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 文件中,这个项目主要是日常学习的一些记录与测试,感兴趣的可以看看。

. . .

相关推荐

额外说明

rand7() 实现 rand10()

题解: 两行代码搞定 int rand10() { int res=(rand7()-1)*7+rand7(); return res<=40?res%10+1:rand10(); }

额外说明

【1++的Linux】之进程(四)

-作者主页:进击的1++ - 专栏链接:【1++的Linux】 文章目录 一,进程创建 二,进程等待 三,进程终止 一,进程创建 在Linux中我们通过fork来创建一个新的进程。新创建的进程叫做子进程,原来的进程叫做父进程。 fork()给父进程返回子

额外说明

图像智能处理黑科技,让图像处理信手拈来

图像智能处理黑科技,让图像处理信手拈来 0. 前言 1. 图像智能处理简介 2. 图像切边增强 3. PS 检测 4. 图像水印去除 5. 图像矫正 6. 图像去屏幕纹 7. 调用图像智能处理 API 小结 0. 前言 计算机视觉 (Computer V

额外说明

探索散列表和哈希表:高效存储与快速检索的魔法

文章目录 散列函数的原理 散列表和哈希表的概念与操作 解决冲突的方法 案例分析:电话簿的实现 拓展:性能与碰撞 结论 -欢迎来到数据结构学习专栏~探索散列表和哈希表:高效存储与快速检索的魔法 ☆* o(≧▽≦)o *☆嗨~我是IT·陈寒- ✨博客主页:I

额外说明

4.讲究先来后到的队列

概述 目标: 队列的存储结构及操作特点 java中队列相关的api 基于单链表的队列实现 刷题(设计循环队列) 存储结构及特点 队列(queue) 和栈一样,代表具有一类操作特征的数据结构,拿日常生活中的一个场景举例说明,去车站的窗口买票,就要排队,先来

额外说明

1、Sentinel基本应用&限流规则(1)

Sentinel基本应用&限流规则 1.1 概述与作用 随着微服务的流行,服务和服务之间的稳定性变得越来越重要。缓存、降级和限流是保护微服务系统运行稳定性的三大利器。 缓存:提升系统访问速度和增大系统能处理的容量 降级:当服务出问题或者影响到核心流程的性

额外说明

机器学习强基计划6-1:图文详细总结马尔科夫链及其性质(附例题分析)

目录 0 写在前面 1 从一个实例出发 2 马尔科夫链 3 马氏链的基本性质 4 C-K方程 5 平稳状态分布 6 遍历性与例题分析 0 写在前面 机器学习强基计划聚焦深度和广度,加深对机器学习模型的理解与应用。“深”在详细推导算法模型背后的数学原理;“

额外说明

5分钟快速了解MySQL索引的各种类型

什么是索引? 索引是数据库存储引擎用于快速查找到指定数据的一种数据结构。 可以用新华字典做类比:如果新华字典中对每个字的详细解释是数据库中表的记录,那么按部首或拼音等排序的目录就是索引,使用它可以让我们快速查找的某一个字详细解释的位置。 在MySQL中,

额外说明

webstorm 入门指南_如何选择最佳的在线销售产品(入门指南)

网络风暴入门指南 You want to make extra money on the side, so you looked into several 网上经营理念 and decided that creating an online store

ads via 小工具