Go 并发编程 - Mutex 注意事项
date
Dec 28, 2022
slug
use-mutex
status
Published
tags
Go
summary
同步原语使用注意事项
type
Post
简介
使用 Mutex 需要注意四个可能踩坑的地方:
- Lock 和 Unlock 没有匹配
- 不可复制
- 不可重入
- 死锁
Lock 和 Unlock 没有匹配
没有匹配指的是 Lock 和 Unlock 不一致
比如说 Lock 之后没有及时的调用 Unlock,导致锁一直被持有,最终死锁 panic。
或者没有 Lock 之前就调用了 Unlock,这样会触发 fatal error,recover 也无能为力。
听起来是很低级的失误,但是在实际开发过程中,可能出现的场景却有很多
- 多层 if-else 的复杂嵌套,导致没有在对应的逻辑分支中释放锁
尽量避免超过 3 层以上的嵌套缩进,如果超过了 3 层,或许应该考虑重构这段逻辑 为什么你不应该嵌套代码,Linux之父也在这样做_哔哩哔哩_bilibili
举一个例子
上面这段代码在 else 的时候忘记释放锁,直接 return,这种情况下会直接导致死锁。
- 重构代码的时候错误的删除了 Unlock
由于不熟悉上下文,在我们进行复杂的逻辑改动的时候,很有可能会不小心删除掉 Unlock 相关的代码,比如说重构上面复杂的 if-else,我们可能尝试删减或添加一个分支的时候,把原有的 Unlock 给去掉了,导致死锁
再举一个例子
- 没有触发 Unlock 的调用逻辑
原始代码逻辑是在 845 行进行加锁,在接下来的子函数中进行处理,并负责解锁
但是这里有一个弊端,就是子函数如果出现 panic,会直接触发 recover 的逻辑,但是旧的 recover 并没有做锁的释放,那么就会出现死锁了。所以这个 MR 也是修复了这个问题,如果出现了 panic,在 recover 的时候同时释放锁,避免死锁出现。
虽然理想状态下是平级做 Lock/Unlock 的,但是工程规模一旦扩大,就很难达到这种理想的重构,所以只能用这种折中的办法修复问题,虽然不是很优雅,但是能解决问题。
- 加锁之前解锁
比如说上面这种情况,会报错
这个 fatal error 是没办法 recover 的,它不是 panic,出现这种情况只能修改代码了。
不可复制
Mutex 的特性是不可复制的,因为 Mutex 中用 state 记录了当前锁的信息,如 waiter 数量,当前锁的状态等信息。而 Mutex 的状态是瞬息万变的,是多个 goruntine 一起决定的,当我们复制了一个 Mutex,可能会得到处于加锁状态的 Mutex,这个 Mutex 永远也没办法使用了。
看一个例子
在函数传递的无意间复制了 Counter 中的 Mutex,这里运行会直接报错
fatal error: all goroutines are asleep - deadlock!
不可重入
锁的可重入特性指的是锁的持有者可以反复获取锁。java 中的 ReentrantLock 就是可重入锁。通过记录锁的持有者和重入次数来进行加锁和解锁。
Go 的 Mutex 不支持可重入,所以连续调用 Lock 也会导致 fatal error。
死锁
这几乎是最常见的问题了。
我们先回忆一下死锁产生的必要条件
- 互斥:竞争的资源具有排他性,不能同时被多个线程占有。
- 请求并保持:每个线程保持了一定的资源,同时还申请新的资源。
- 不可剥夺:一旦资源分配给线程,就无法从它手中剥夺资源。
- 循环等待:多个线程处于循环等待状态,都在等待对方释放资源。
扩展
Go 的死锁检查机制
Go 能够在运行时检查出死锁问题,能够打印出堆栈信息
如果我们想要在运行之前检测有没有死锁问题,可以使用 vet 这个工具
当使用 vet 之后,这个工具可以提示我们潜在的死锁问题,非常智能