Go Mutex功能拓展
首先要明确锁是性能下降的“罪魁祸首”之一,所以,有效地降低锁的竞争,就能够很好地提高性能。因此,监控关键互斥锁上等待的 goroutine 的数量,是我们分析锁竞争的激烈程度的一个重要指标
TryLock
我们可以为 Mutex 添加一个 TryLock 的方法,也就是尝试获取排外锁,当一个 goroutine 调用这个 TryLock 方法请求锁的时候,如果这把锁没有被其他 goroutine 所持有,那么,这个 goroutine 就持有了这把锁,并返回 true;如果这把锁已经被其他 goroutine 所持有,或 者是正在准备交给某个被唤醒的 goroutine,那么,这个请求锁的 goroutine 就直接返回 false,不会阻塞在方法调用上。
// 复制Mutex定义的常量
const (
mutexLocked = 1 << iota // 加锁标识位置
mutexWoken // 唤醒标识位置
mutexStarving // 锁饥饿标识位置
mutexWaiterShift = iota // 标识waiter的起始bit位置
)
// 扩展一个Mutex结构
type Mutex struct {
sync.Mutex
}
// 尝试获取锁
func (m *Mutex) TryLock() bool {
// 如果能成功抢到锁
if atomic.CompareAndSwapInt32((*int32)(unsafe.Pointer(&m.Mutex)), 0, mutexWoken){
return true
}
// 如果处于唤醒、加锁或者饥饿状态,这次请求就不参与竞争了,返回false
old := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.Mutex)))
if old&(mutexLocked|mutexStarving|mutexWoken) != 0 {
return false
}
// 尝试在竞争的状态下请求锁
new := old | mutexLocked
return atomic.CompareAndSwapInt32((*int32)(unsafe.Pointer(&m.Mutex)), old,new)
}
第 17 行是一个 fast path,如果幸运,没有其他 goroutine 争这把锁,那么,这把锁就会 被这个请求的 goroutine 获取,直接返回。 如果锁已经被其他 goroutine 所持有,或者被其他唤醒的 goroutine 准备持有,那么,就 直接返回 false,不再请求,代码逻辑在第 23 行。 如果没有被持有,也没有其它唤醒的 goroutine 来竞争锁,锁也不处于饥饿状态,就尝试 获取这把锁(第 29 行),不论是否成功都将结果返回。因为,这个时候,可能还有其他的 goroutine 也在竞争这把锁,所以,不能保证成功获取这把锁。
func TestTryLock(t *testing.T) {
var mu Mutex
go func() { // 启动一个goroutine持有一段时间的锁
mu.Lock()
time.Sleep(time.Duration(rand.Intn(2)) * time.Second)
mu.Unlock()
}()
time.Sleep(time.Second)
ok := mu.TryLock() // 尝试获取到锁
if ok { // 获取成功
fmt.Println("got the lock")
// do something
mu.Unlock()
return
}
// 没有获取到
fmt.Println("can't get the lock")
}
获取等待者的数量等指标
mutex包含两个字段,state 和 sema。前四个字节(int32)就是 state 字段。
type Mutex struct {
state int32
sema uint32
}
Mutex 结构中的 state 字段有很多个含义,通过 state 字段,你可以知道锁是否已经被某 个 goroutine 持有、当前是否处于饥饿状态、是否有等待的 goroutine 被唤醒、等待者的 数量等信息。但是,state 这个字段并没有暴露出来,所以,我们需要想办法获取到这个字 段,并进行解析
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
)
type Mutex struct {
sync.Mutex
}
func (m *Mutex) Count() int {
// 获取state字段的值
v := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.Mutex)))
v = v >> mutexWaiterShift //得到等待者的数值
v = v + (v & mutexLocked) //再加上锁持有者的数量,0或者1
return int(v)
}
第 14 行通过 unsafe 操作,我们可以得到 state 字段的值。第 15 行我们右移 三位(这里的常量 mutexWaiterShift 的值为 3),就得到了当前等待者的数量。如果当前 的锁已经被其他 goroutine 持有,那么,我们就稍微调整一下这个值,加上一个 1(第 16 行),你基本上可以把它看作是当前持有和等待这把锁的 goroutine 的总数
state 这个字段的第一位是用来标记锁是否被持有,第二位用来标记是否已经唤醒了一个等 待者,第三位标记锁是否处于饥饿状态,通过分析这个 state 字段我们就可以得到这些状 态信息。我们可以为这些状态提供查询的方法,这样就可以实时地知道锁的状态了。
// 锁是否被持有
func (m *Mutex) IsLocked() bool {
state := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.Mutex)))
return state&mutexLocked == mutexLocked
}
// 是否有等待者被唤醒
func (m *Mutex) IsWoken() bool {
state := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.Mutex)))
return state&mutexWoken == mutexWoken
}
// 锁是否处于饥饿状态
func (m *Mutex) IsStarving() bool {
state := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.Mutex)))
return state&mutexStarving == mutexStarving
}
我们可以写一个程序测试一下,比如,在 1000 个 goroutine 并发访问的情况下,我们可 以把锁的状态信息输出出来
func count() {
var mu Mutex
for i := 0; i < 1000; i++ { // 启动1000个goroutine
go func() {
mu.Lock()
time.Sleep(time.Second)
mu.Unlock()
}()
}
time.Sleep(time.Second)
// 输出锁的信息
fmt.Printf("waitings: %d, isLocked: %t, woken: %t, starving: %t\n",mu.Count())
}
有一点你需要注意一下,在获取 state 字段的时候,并没有通过 Lock 获取这把锁,所以获 取的这个 state 的值是一个瞬态的值,可能在你解析出这个字段之后,锁的状态已经发生 了变化。不过没关系,因为你查看的就是调用的那一时刻的锁的状态。
使用 Mutex 实现一个线程安全的队列
队列,我们可以通过 Slice 来实现,但是通过 Slice 实现的队列不是线程安全的,出队(Dequeue)和入队(Enqueue)会有 data race 的问题。这个时候,Mutex 就要隆重出场了,通过它,我们可以在出队和入队的时候加上锁的保护
type SliceQueue struct {
data []interface{}
mu sync.Mutex
}
func NewSliceQueue(n int) (q *SliceQueue) {
return &SliceQueue{data: make([]interface{}, 0, n)}
}
// Enqueue 把值放在队尾
func (q *SliceQueue) Enqueue(v interface{}) {
q.mu.Lock()
q.data = append(q.data, v)
q.mu.Unlock()
}
// Dequeue 移去队头并返回
func (q *SliceQueue) Dequeue() interface{} {
q.mu.Lock()
if len(q.data) == 0 {
q.mu.Unlock()
return nil
}
v := q.data[0]
q.data = q.data[1:]
q.mu.Unlock()
return v
}
你可以为 Mutex 获取锁时加上 Timeout 机制吗?会有什么问题吗?
type Mutex struct {
ch chan struct{}
}
func NewMutex() *Mutex {
return &Mutex{make(chan struct{}, 1)}
}
func (m *Mutex) Lock() {
m.ch <- struct{}{}
}
func (m *Mutex) Unlock() {
<-m.ch
}
func (m *Mutex) TryLock(timeout time.Duration) error {
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case m.ch <- struct{}{}:
return nil
case <-timer.C:
return errors.New("lock timeout")
}
}
这种做法可能会对性能产生一定的影响,因为每次获取锁都需要启动一个协程。此外,使用 Timeout 机制可能会导致竞态条件的出现,因为多个协程可能同时尝试获取同一个资源。因此,需要根据具体的场景和需求来选择是否使用 Timeout 机制