使用Mutex锁时常见错误:
- 非成对出现
- copy某个有状态的Mutex
- 锁的重入
- 死锁
如下为一个错误的重入锁的使用案例:
func foo(l sync.Locker) {
fmt.Println("in foo")
l.Lock()
bar(l)
l.Unlock()
}
func bar(l sync.Locker) {
l.Lock()
fmt.Println("in bar")
l.Unlock()
}
func main() {
l := &sync.Mutex{}
foo(l)
}
设计一个可重入的Mutex锁
这里的关键就是,实现的锁要能记住当前是哪个 goroutine 持有这个锁。有两个方案:
- 通过haker的方式(即非常规模式)获取到goroutine Id,记录下获取锁的goroutine Id,它可以实现Locker接口。
- 调用Lock、Unlock方式时,额外提供一个token参数,用来标识获取锁的goroutine, 但这样一来就不满足Locker接口了。
方案一:goroutine id
获取方式有两种,一种简单方式,可以通过runtime.Stack获取帧栈信息,其中包含goroutine id。
func GoID() int {
var buf [64]byte
n := runtime.Stack(buf[:], false)
idField := strings.Fields(strings.TrimPrefix(buf[:n], "goroutine"))[0]
id, err := strconv.Atoi(idField)
if err != nil {
panic(fmt.Sprintf("cannot get goroutine id: %v", err))
}
return id
}
另一种方式:
首先,我们获取运行时的 g 指针,反解出对应的 g 的结构。每个运行的 goroutine 结构的 g 指针保存在当前 goroutine 的一个叫做 TLS 对象中。第一步:我们先获取到 TLS 对象;第二步:再从 TLS 中获取 goroutine 结构的 g 指针;第三步:再从 g 指针中取出 goroutine id。
需要注意的是,不同 Go 版本的 goroutine 的结构可能不同,所以需要根据 Go 的不同版本进行调整。当然了,如果想要搞清楚各个版本的 goroutine 结构差异,所涉及的内容又过于底层而且复杂,学习成本太高。怎么办呢?我们可以重点关注一些库。我们没有必要重复发明*,直接使用第三方的库来获取 goroutine id 就可以了。好消息是现在已经有很多成熟的方法了,可以支持多个 Go 版本的 goroutine id,给你推荐一个常用的库:petermattis/goid。
获取到goroutine id后,接下来就是设计可重入的Mutex锁。
func (m *RecursiveMutext) Lock() {
gid := goid.Get()
if atomic.LoadInt64(&m.owner) == gid { // 如果是自己
m.recursion++
return
}
m.Mutex.Lock()
atomic.StoreInt64(&m.owner, gid)
m.recursion = 1
}
func (m *RecursiveMutext) Unlock() {
gid := goid.Get()
if atomic.LoadInt64(&m.owner) != gid {
panic(fmt.Sprintf("wrong the owner(%d): %d!", m.owner, gid))
}
m.recursion--
if m.recursion != 0 {
return
}
atomic.StoreInt64(&m.owner, -1)
m.Mutex.Unlock()
方案二:token
// Token方式的递归锁
type TokenRecursiveMutex struct {
sync.Mutex
token int64
recursion int32
}
// 请求锁,需要传入token
func (m *TokenRecursiveMutex) Lock(token int64) {
if atomic.LoadInt64(&m.token) == token { //如果传入的token和持有锁的token一致,说明是递归调用
m.recursion++
return
}
m.Mutex.Lock() // 传入的token不一致,说明不是递归调用
// 抢到锁之后记录这个token
atomic.StoreInt64(&m.token, token)
m.recursion = 1
}
// 释放锁
func (m *TokenRecursiveMutex) Unlock(token int64) {
if atomic.LoadInt64(&m.token) != token { // 释放其它token持有的锁
panic(fmt.Sprintf("wrong the owner(%d): %d!", m.token, token))
}
m.recursion-- // 当前持有这个锁的token释放锁
if m.recursion != 0 { // 还没有回退到最初的递归调用
return
}
atomic.StoreInt64(&m.token, 0) // 没有递归调用了,释放锁
m.Mutex.Unlock()
}