头图

foreword

golang 的sync包下有种锁, sync.RWMutex , sync.Mutex ,本文将讲解下sync.Mutex是How? How to avoid read/write starvation problem? Let us take these questions to see how the source code is implemented

Overview

 // Mutex fairness.
//
// Mutex can be in 2 modes of operations: normal and starvation.
// In normal mode waiters are queued in FIFO order, but a woken up waiter
// does not own the mutex and competes with new arriving goroutines over
// the ownership. New arriving goroutines have an advantage -- they are
// already running on CPU and there can be lots of them, so a woken up
// waiter has good chances of losing. In such case it is queued at front
// of the wait queue. If a waiter fails to acquire the mutex for more than 1ms,
// it switches mutex to the starvation mode.
//
// In starvation mode ownership of the mutex is directly handed off from
// the unlocking goroutine to the waiter at the front of the queue.
// New arriving goroutines don't try to acquire the mutex even if it appears
// to be unlocked, and don't try to spin. Instead they queue themselves at
// the tail of the wait queue.
//
// If a waiter receives ownership of the mutex and sees that either
// (1) it is the last waiter in the queue, or (2) it waited for less than 1 ms,
// it switches mutex back to normal operation mode.
//
// Normal mode has considerably better performance as a goroutine can acquire
// a mutex several times in a row even if there are blocked waiters.
// Starvation mode is important to prevent pathological cases of tail latency.

The above is excerpted from the comments on mutex in the golang source code. I think it is very clear to summarize and explain

Mutex As the lock in the concurrency primitive, it involves the fairness of the lock (that is, the fair lock and the unfair lock, usually the performance of the unfair lock is better), it is called two modes in go: 正常 饥饿 .

In normal mode , goroutines that have not acquired the lock will be queued in the queue in a FIFO manner in the waiter. When the lock is released, it will wake up the goroutine in the waiter, and it will compete with the new goroutine (if the lock is released, there will be a new goroutine to acquire the lock), and the new goroutine has a greater advantage to obtain the lock. locks because they are being executed by the CPU. Then the goroutine that just woke up in the waiter does not acquire the lock (runs in vain), then it will be placed in the queue head of the waiter. When the goroutine in the waiter does not acquire the lock for more than 1s, the mutex will be set to starvation model.

In starvation mode , in the process of releasing the lock, the new goroutine will not participate in the competition for the lock, and the goroutine at the head of the waiter queue will directly acquire the lock. If the waiting time of the goroutine at the head of the queue is less than 1ms, it means that there is no coroutine starving at this time. , will switch back to normal mode.

source code

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

type Mutex struct {
   state int32
   sema  uint32
}

state The lower three bits in the state are used to identify the state of the lock, and the other high bits are used to record the number of waiters. The state can be expressed as: waiterNum|mutexStarving|mutexWoken|mutexLocked

sema is a FIFO queue for goroutines to queue here as a waiter.

acquire lock

No competition, locks are acquired directly through CAS

 func (m *Mutex) Lock() {
   // Fast path: grab unlocked mutex.
   if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
      if race.Enabled {
         race.Acquire(unsafe.Pointer(m))
      }
      return
   }
   // Slow path (outlined so that the fast path can be inlined)
   m.lockSlow()
}

There is competition, go lockSlow

normal mode
 if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
   // Active spinning makes sense.
   // Try to set mutexWoken flag to inform Unlock
   // to not wake other blocked goroutines.
   if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
      atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
      awoke = true
   }
   runtime_doSpin()
   iter++
   old = m.state
   continue
}

In normal mode, the spin waits for the release of the lock, and the state is set to mutexWoken , which is used to determine whether the lock resource can be handed over to the coroutine competition lock of the spin lock when the lock is released.

 if atomic.CompareAndSwapInt32(&m.state, old, new) {
   if old&(mutexLocked|mutexStarving) == 0 {
     // 这里是正常模式下,线程唤醒后获取到锁的出口
      break // locked the mutex with CAS //线程自旋后,原来持有锁的线程释放锁后,state的mutexLocked 或置于0。然后,本次CAS 成功,获取到锁
   }
   // If we were already waiting before, queue at the front of the queue.  //没有获取到锁,若是之前已经在waiter中,则放入队首,否则放入队尾
   queueLifo := waitStartTime != 0
   if waitStartTime == 0 {
      waitStartTime = runtime_nanotime()
   }
   runtime_SemacquireMutex(&m.sema, queueLifo, 1)  //每次有锁释放,会唤醒waiter 协程,唤醒点在这里
   starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
   old = m.state
    ...
   awoke = true
   iter = 0
}
  1. After the thread spins, after the thread that originally held the lock releases the lock, the mutexLocked of the state may be set to 0. Then, this CAS succeeds and the lock is acquired
  2. If the waiter wakes up, but the lock is not acquired, put it at the head of the waiter team, otherwise put it at the tail of the team
  3. If the waiting time exceeds 1s, switch the mutex to starvation mode
starvation mode
 new := old
...
if atomic.CompareAndSwapInt32(&m.state, old, new) {
   ...
   starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
   old = m.state
   if old&mutexStarving != 0 {
      ...
      delta := int32(mutexLocked - 1<<mutexWaiterShift)
      if !starving || old>>mutexWaiterShift == 1 { 
         // Exit starvation mode.
         // Critical to do it here and consider wait time.
         // Starvation mode is so inefficient, that two goroutines
         // can go lock-step infinitely once they switch mutex
         // to starvation mode.
         delta -= mutexStarving
      }
      atomic.AddInt32(&m.state, delta)
      break  //这里是锁在饥饿条件下,协程被唤醒后(获取到锁)的出口
   }
   awoke = true
   iter = 0
} else {
   old = m.state
}
  1. If the coroutine has not acquired the lock for more than 1ms, switch to starvation mode ( runtime_nanotime()-waitStartTime > starvationThresholdNs ).
  2. If there is only this coroutine left in the waiter queue, then exit the starvation mode ( old>>mutexWaiterShift == 1 )

release lock

No lock competition, direct CAS releases lock resources

 func (m *Mutex) Unlock() {
   // Fast path: drop lock bit.
   new := atomic.AddInt32(&m.state, -mutexLocked)
   if new != 0 {
      // Outlined slow path to allow inlining the fast path.
      // To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
      m.unlockSlow(new)
   }
}

There is competition, go unlockSlow

normal mode
 old := new
for {
   // If there are no waiters or a goroutine has already
   // been woken or grabbed the lock, no need to wake anyone.
   // In starvation mode ownership is directly handed off from unlocking
   // goroutine to the next waiter. We are not part of this chain,
   // since we did not observe mutexStarving when we unlocked the mutex above.
   // So get off the way.
   if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
      return
   }
   // Grab the right to wake someone.
   new = (old - 1<<mutexWaiterShift) | mutexWoken
   if atomic.CompareAndSwapInt32(&m.state, old, new) {
      runtime_Semrelease(&m.sema, false, 1)
      return
   }
   old = m.state
}

Arouse one from the waiter and compete for lock resources with the new goroutine

starvation mode
 // Starving mode: handoff mutex ownership to the next waiter, and yield
// our time slice so that the next waiter can start to run immediately.
// Note: mutexLocked is not set, the waiter will set it after wakeup.
// But mutex is still considered locked if mutexStarving is set,
// so new coming goroutines won't acquire it.
runtime_Semrelease(&m.sema, true, 1)

Take the first hungry coroutine waiting for the queue directly from the waiter to acquire the lock

references

  1. https://juejin.cn/post/6958979192574705701
  2. https://segmentfault.com/a/1190000039855697

John
13 声望4 粉丝