Go-Mutex锁常见错误

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

上一篇:《Go语言并发之道》学习笔记之第6章 goroutine和Go语言运行时


下一篇:学习中记录差异—java和golang并发的不同点