Go语言中如何检查互斥锁是否被锁定:实用调试技巧

本文详细介绍了在Go语言中检查sync.Mutex和sync.RWMutex锁状态的实现方法,通过反射机制访问未导出字段,提供完整的代码示例和调试构建的最佳实践方案。

如何检查Go中的互斥锁是否被锁定

TL;DR: 我们能在Go中检查互斥锁是否被锁定吗?是的,但不能使用互斥锁API。这里有一个用于调试构建的解决方案。

虽然你可以对互斥锁执行Lock()Unlock()操作,但无法检查它是否已被锁定。尽管这是一个合理的省略(例如,由于可能的竞态条件;另见为什么不能检查互斥锁是否被锁定?),但拥有这样的功能对于测试软件是否按预期工作仍然很有用。

换句话说,最好有一个专门用于调试构建的AssertMutexLocked函数,可以这样使用:

1
2
3
4
5
// 这个方法应该总是在o.lock锁定的情况下调用
func (o *Object) someMethodImpl() {
    AssertMutexLocked(&o.lock)
    // (...)
}

拥有这样的函数将使我们能够确认给定互斥锁已被锁定的假设,并在将其添加到现有代码库时发现潜在的错误。事实上,在官方Go仓库中有一个关于添加此确切功能的GitHub问题(golang/go#1366),但它以WontFix状态关闭。

我还通过优秀的grep.app项目了解到,许多项目都有类似的互斥锁前置条件,例如google/gvisor、ghetovoice/gossip、vitessio/vitess等。

现在让我们实现MutexLocked(和其他)函数。

检查互斥锁是否被锁定

要检查互斥锁是否被锁定,我们必须读取其状态。sync.Mutex结构包含两个字段:

1
2
3
4
type Mutex struct {
    state int32
    sema  uint32
}

state字段的位对应以下标志(来源):

1
2
3
4
5
6
7
const (
    mutexLocked = 1 << iota // 互斥锁已锁定
    mutexWoken
    mutexStarving
    mutexWaiterShift = iota
    // (...)
)

因此,如果互斥锁被锁定,其state字段设置了mutexLocked(1)位。但是,我们不能直接从Go程序访问state字段,因为该字段未导出(其名称不以大写字母开头)。幸运的是,仍然可以使用Go反射访问该字段,我在下面的代码中实现了允许我们检查给定sync.Mutexsync.RWMutex是否被锁定的函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import (
    "fmt"
    "reflect"
    "sync"
)

const mutexLocked = 1

func MutexLocked(m *sync.Mutex) bool {
    state := reflect.ValueOf(m).Elem().FieldByName("state")
    return state.Int()&mutexLocked == mutexLocked
}

func RWMutexWriteLocked(rw *sync.RWMutex) bool {
    // RWMutex有一个用于写锁的"w" sync.Mutex字段
    state := reflect.ValueOf(rw).Elem().FieldByName("w").FieldByName("state")
    return state.Int()&mutexLocked == mutexLocked
}

func RWMutexReadLocked(rw *sync.RWMutex) bool {
    return reflect.ValueOf(rw).Elem().FieldByName("readerCount").Int() > 0
}

func main() {
    m := sync.Mutex{}
    fmt.Println("m locked =", MutexLocked(&m))
    m.Lock()
    fmt.Println("m locked =", MutexLocked(&m))
    m.Unlock()
    fmt.Println("m locked =", MutexLocked(&m))

    rw := sync.RWMutex{}
    fmt.Println("rw write locked =", RWMutexWriteLocked(&rw), " read locked =", RWMutexReadLocked(&rw))
    rw.Lock()
    fmt.Println("rw write locked =", RWMutexWriteLocked(&rw), " read locked =", RWMutexReadLocked(&rw))
    rw.Unlock()
    fmt.Println("rw write locked =", RWMutexWriteLocked(&rw), " read locked =", RWMutexReadLocked(&rw))
    rw.RLock()
    fmt.Println("rw write locked =", RWMutexWriteLocked(&rw), " read locked =", RWMutexReadLocked(&rw))
    rw.RLock()
    fmt.Println("rw write locked =", RWMutexWriteLocked(&rw), " read locked =", RWMutexReadLocked(&rw))
    rw.RUnlock()
    fmt.Println("rw write locked =", RWMutexWriteLocked(&rw), " read locked =", RWMutexReadLocked(&rw))
    rw.RUnlock()
    fmt.Println("rw write locked =", RWMutexWriteLocked(&rw), " read locked =", RWMutexReadLocked(&rw))
}

我们可以看到这个程序的输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
m locked = false
m locked = true
m locked = false
rw write locked = false  read locked = false
rw write locked = true  read locked = false
rw write locked = false  read locked = false
rw write locked = false  read locked = true
rw write locked = false  read locked = true
rw write locked = false  read locked = true
rw write locked = false  read locked = false

这可以用于创建AssertMutexLocked和其他函数。为此,我在trailofbits/go-mutexasserts创建了一个包含这些函数的小型库——它仅在带有调试标签的构建中启用断言检查。

注意:尽管Go还有其他检测竞态条件的工具,例如Go的竞态检测器或Trail of Bits的OnEdge,但这些工具只会在问题情况发生时检测到,不允许你断言互斥锁前置条件是否成立。

我们一直在开发工具来帮助你更快更智能地工作。需要为你的下一个项目提供帮助吗?联系我们!

如果你喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计