Go并发5 同步原语 - Mutex 可重入锁的设计与实现

148 阅读3分钟
如何自定义一个可重入的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()
}