如何自定义一个可重入的Mutex?
实现可重入锁一个关键点
我们实现的锁要能标记当前是哪个 goroutine 持有这个锁。
记录获取协程标记的两个方案
方案一:
通过 runtime.Stack 或者 hacker 的方式获取到 goroutine id,加锁时记录下获取锁的 goroutine id,它可以同时实现 Locker 接口。
runtime.Stack
通过 runtime.Stack 方法获取栈帧信息,栈帧信息里包含 goroutine id。
func GoID() int {
var buf [64]byte
n := runtime.Stack(buf[:], false)
// 得到id字符串
str := string(buf[:n])
prefix := "goroutine "
idField := strings.Fields(strings.TrimPrefix(str, prefix))[0]
//str to int
id, err := strconv.Atoi(idField)
if err != nil {
panic(fmt.Sprintf("cannot get goroutine id: %v", err))
}
return id
}
func main() {
fmt.Println(GoID())
}
Hacker方式
每个运行的 goroutine 结构的 g 指针保存在当前 goroutine 的一个叫做 TLS 对象中,所以我们可以从TLS对象入手进而获取到gid。
Hacker获取gid的三个步骤 1.先获取到 TLS 对象; 2.再从 TLS 中获取 goroutine 结构的 g 指针; 3、再从 g 指针中取出 goroutine id。
petermattis/goid
事实上随着Go版本的更迭,协程对象的结构也可能发生变化,所以推荐使用一些现成的库区获取gid,比如petermattis/goid
//install and import
go env -w GOPROXY=https://goproxy.cn
go get -u github.com/petermattis/goid
import
(
"github.com/petermattis/goid"
)
可重入Mutex实例
// RecursiveMutex 包装一个Mutex,实现可重入
type RecursiveMutex struct {
sync.Mutex
owner int64 // 标记当前持有锁的goroutine id
recursion int32 // 这个goroutine 重入的次数
}
func (m *RecursiveMutex) Lock() {
//获取当前gid
gid := goid.Get()
// 如果当前持有锁的goroutine就是这次调用的goroutine,说明是重入
if atomic.LoadInt64(&m.owner) == gid {
//重入次数++, 实际上如果超过最大重入次数我们需要抛错
m.recursion++
return
}
m.Mutex.Lock()
// 获得锁的goroutine第一次调用,记录下它的goroutine id,调用次数加1
atomic.StoreInt64(&m.owner, gid)
m.recursion = 1
}
func (m *RecursiveMutex) Unlock() {
//获取当前gid
gid := goid.Get()
// 非持有锁的goroutine尝试释放锁,错误的使用,只有锁的持有者才能进行锁的释放
if atomic.LoadInt64(&m.owner) != gid {
panic(fmt.Sprintf("wrong the owner(%d): %d!", m.owner, gid))
}
// 调用次数减1
m.recursion--
if m.recursion != 0 { // 如果这个goroutine还没有完全释放,则直接返回
return
}
// 此goroutine最后一次调用,需要释放锁
atomic.StoreInt64(&m.owner, -1)
m.Mutex.Unlock()
}
方案二:
调用 Lock/Unlock 方法时,由 goroutine 提供一个 token,用来标识它自己,但是就不满足 Locker 接口了。
从Go的设计来说,Go 的开发者不期望你利用 goroutine id 做一些不确定的东西,所以他们没有暴露获取 goroutine id 的方法,我们可以尝试使用token替换gid的方式来实现对携程的标记。
token可重入锁实例
该方案跟方案一的唯一区别在于,调用者在获取锁时或者释放锁时需要传入一个自己的token,通过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()
}