Detail the internal implementation of the Golang mutex

  • 2020-06-03 06:55:03
  • OfStack

go language offers a way out of the box to a Shared resource, the mutex (sync. Mutex), sync. Mutex zero said one not locked, can use directly, 1 goroutine acquire a mutex other goroutine can only wait until after the gorutine released the mutex, open only two functions in the Mutex structure, respectively is Lock and Unlock, when using a mutex is very simple, use the purpose of this article is not.

Never make a copy of the value when using sync.Mutex as this may cause the lock to fail. When we open our IDE and jump into our ES18en.Mutex code we will see that it has the following structure:


type Mutex struct {
 state int32   // The enumeration values of the lock state on the mutex are shown below 
 sema uint32  // Semaphore Gwaitting the G Send a signal 
}

const (
 mutexLocked = 1 << iota // 1  A mutex is locked 
 mutexWoken       // 2  Wake up the lock 
 mutexWaiterShift = iota // 2  Statistics are blocked on this mutex goroutine The number of values to shift 
)

The state values above are 0(available) 1(locked) 2~31 wait queue count

The following is the source code of the mutex, there will be four more important methods to explain in advance, they are runtime_canSpin, runtime_doSpin, runtime_SemacquireMutex, runtime_Semrelease,

1. runtime_canSpin: Conservative spin, spin lock in golang does not spin 1 straight down, runtime_canSpin method in runtime package makes some restrictions, the iter passed is at most 4 or the cpu core is at least 1, the maximum logical processor is greater than 1, there is at least a local P queue, and the local P queue can run G queue is empty.


//go:linkname sync_runtime_canSpin sync.runtime_canSpin
func sync_runtime_canSpin(i int) bool {
 if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {
 return false
 }
 if p := getg().m.p.ptr(); !runqempty(p) {
 return false
 }
 return true
}

2, runtime_doSpin: will call procyield function, which is also assembly language implementation. The PAUSE instruction is called in a loop within the function. The PAUSE directive does nothing, but consumes CPU time, and CPU does not optimize it unnecessarily when it executes the PAUSE directive.


//go:linkname sync_runtime_doSpin sync.runtime_doSpin
func sync_runtime_doSpin() {
 procyield(active_spin_cnt)
}

3, runtime_SemacquireMutex:


//go:linkname sync_runtime_SemacquireMutex sync.runtime_SemacquireMutex
func sync_runtime_SemacquireMutex(addr *uint32) {
 semacquire(addr, semaBlockProfile|semaMutexProfile)
}

4, runtime_Semrelease:


//go:linkname sync_runtime_Semrelease sync.runtime_Semrelease
func sync_runtime_Semrelease(addr *uint32) {
 semrelease(addr)
}
Mutex the Lock The function definition is as follows 

func (m *Mutex) Lock() {
    // Use the first CAS Attempt to acquire a lock 
 if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
        // Here is the -race Don't worry about it 
 if race.Enabled {
  race.Acquire(unsafe.Pointer(m))
 }
        // Successful retrieval return 
 return
 }

 awoke := false // Circulating markers 
 iter := 0    // Cycle counter 
 for {
 old := m.state // Gets the current lock state 
 new := old | mutexLocked // Put the current state last 1 A specified 1
 if old&mutexLocked != 0 { // If so is occupied 
  if runtime_canSpin(iter) { // Check to see if the spin lock is accessible 
  if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
   atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) { 
                    //awoke Marked as true
   awoke = true
  }
                // I'm going to spin 
  runtime_doSpin()
  iter++
  continue
  }
            // No lock was obtained, currently G Enter the Gwaitting state 
  new = old + 1<<mutexWaiterShift
 }
 if awoke {
  if new&mutexWoken == 0 {
  throw("sync: inconsistent mutex state")
  }
            // Remove tag 
  new &^= mutexWoken
 }
        // Update the status 
 if atomic.CompareAndSwapInt32(&m.state, old, new) {
  if old&mutexLocked == 0 {
  break
  }
             
            //  Lock request failed , Go into hibernation , Wait for the signal to wake up and start the cycle again 
  runtime_SemacquireMutex(&m.sema)
  awoke = true
  iter = 0
 }
 }

 if race.Enabled {
 race.Acquire(unsafe.Pointer(m))
 }
}
Mutex the Unlock The function definition is as follows 

func (m *Mutex) Unlock() {
 if race.Enabled {
 _ = m.state
 race.Release(unsafe.Pointer(m))
 }

 //  Remove tag 
 new := atomic.AddInt32(&m.state, -mutexLocked)
 if (new+mutexLocked)&mutexLocked == 0 {
 throw("sync: unlock of unlocked mutex")
 }

 old := new
 for {
 // When the wait count in the sleep queue is 0 Or the spin state counter is 0 To quit 
 if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken) != 0 {
  return
 }
 //  Reduce wait times and add a clear flag 
 new = (old - 1<<mutexWaiterShift) | mutexWoken
 if atomic.CompareAndSwapInt32(&m.state, old, new) {
            //  Release the lock , Send release signal 
  runtime_Semrelease(&m.sema)
  return
 }
 old = m.state
 }
}

The mutex non-conflict is the simplest case, when there is a conflict, spin first, because most Mutex protected code segments are very short, after a short spin can be obtained; If the spin wait fails, the semaphore is used to get the current Goroutine into Gwaitting.


Related articles: