Go并发6 同步原语 - Mutex 通过Hacker的方式拓展额外功能

113 阅读2分钟
互斥锁堵塞带来的性能下降

我们知道互斥锁如果被某个 goroutine 获取了,而且还没有释放,其他请求这把锁的 goroutine,就会阻塞等待,直到有机会获得这把锁。

有时候阻塞并不是一个好的选择,比如你请求锁更新一个计数器,如果获取不到锁的话没必要等待,大不了这次不更新,我下次更新就好了,如果阻塞的话会导致业务处理能力的下降。

TryLock - 尝试获取排外锁

为了解决上述问题,我们可以增加一个TryLock方法,实现非阻塞的尝试获取锁方法,

TryLock工作流程

  1. goroutine 调用TryLock 方法请求锁。
  2. 如果这把锁没有被其他 goroutine 所持有,goroutine 加锁成功,返回 true;
  3. 如果这把锁已经被其他 goroutine 所持有,或者是正在准备交给某个被唤醒的 goroutine,这个请求锁的 goroutine 就直接返回 false,不会阻塞在方法调用上。

实现
TryLock方法

package main

import (
   "fmt"
   "math/rand"
   "sync"
   "sync/atomic"
   "time"
   "unsafe"
)

// 复制Mutex定义的常量
const (
   mutexLocked      = 1 << iota // 加锁标识位置
   mutexWoken                   // 唤醒标识位置
   mutexStarving                // 锁饥饿标识位置
   mutexWaiterShift = iota      // 标识waiter的起始bit位置
)

// Mutex 扩展一个Mutex结构
type Mutex struct {
   sync.Mutex
}

// TryLock 尝试获取锁
func (m *Mutex) TryLock() bool {
   // 如果能成功抢到锁
   //fast path
   if atomic.CompareAndSwapInt32((*int32)(unsafe.Pointer(&m.Mutex)), 0, mutexLocked) {
      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)
}

测试
定义一个协程持有锁,锁将会在随机时间内释放,另一个协程尝试获取锁,如果持有锁的协程在1s内释放,则获取成功,否则获取失败。

func try() {
   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, return directly!")
}

func main() {
   try()
}

实际场景应用
当我们需要并发的去修改配置文件时,我们想避免有多个额Gorotgine修改文件。
当某个 goroutine 想要更改配置数据时,如果发现已经有 goroutine 在更改了,其他的 goroutine 调用 TryLock,返回了 false,这个 goroutine 就会放弃更改。

如何监控Waiter的数量
我们前面说到锁竞争程度是跟性能成反比的,所以监控关键互斥锁上等待的 goroutine 的数量,是我们分析锁竞争的激烈程度的一个重要指标。

Muntex结构
Mutex包含两个字段,state 和 sema。前四个字节(int32)就是 state 字段,Mutex 结构中的 state 字段有很多个含义,通过 state 字段,你可以知道锁是否已经被某个 goroutine 持有、当前是否处于饥饿状态、是否有等待的 goroutine 被唤醒、等待者的数量等信息,我们可以通过unsafe的方法获取到这些信息。

type Mutex struct {
 state int32
 sema uint32
}

Count实现

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)))
   //右移三位(这里的常量 mutexWaiterShift 的值为 3)得到了当前等待者的数量
   //如果当前的锁已经被其他 goroutine 持有,加1
   v = v>>mutexWaiterShift + (v & mutexLocked)
   return int(v)
}

锁的状态判断
state 这个字段的第一位是用来标记锁是否被持有,第二位用来标记是否已经唤醒了一个等待者,第三位标记锁是否处于饥饿状态。

实现

const (
   mutexLocked = 1 << iota // mutex is locked
   mutexWoken
   mutexStarving
   mutexWaiterShift = iota
)

// IsLocked 锁是否被持有
func (m *Mutex) IsLocked() bool {
   state := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.Mutex)))
   return state&mutexLocked == mutexLocked
}

// IsWoken 是否有等待者被唤醒
func (m *Mutex) IsWoken() bool {
   state := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.Mutex)))
   return state&mutexWoken == mutexWoken
}

// IsStarving 锁是否处于饥饿状态
func (m *Mutex) IsStarving() bool {
   state := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.Mutex)))
   return state&mutexStarving == mutexStarving
}

测试

func PrintCount() {
   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(), mu.IsLocked(), mu.IsWoken(), mu.IsStarving())
}

func main() {
   PrintCount()
}

result:
waitings: 999, isLocked: true, woken: false, starving: false

实现一个线程安全的队列
Go的标准库中没有线程安全的队列数据结构的实现,我们可以通过 Mutex 实现一个简单的队列。通过 Mutex 我们可以为一个非线程安全的 data interface{}实现线程安全的访问。

实现

package main

import "sync"

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
}