- Mutex (mutex) details: mutex is a value type that implements the locker interface, so you need to pay attention to parameter transmission when using it. Its bottom layer is nested with linux semaphores. Each operation is actually a PV operation
type Mutex struct { state int32 sema uint32 }
- Semaphore interpretation
By operating semaphores S To deal with the synchronization and mutual exclusion between processes. S>0: Indicates yes S Resources available; S=0 Indicates that no resources are available; S<0 Absolute value indicates the number of processes in the waiting queue or linked list. Semaphore S The initial value of should be greater than or equal to 0. P Primitive: it means to apply for a resource, right S Atomic minus 1, If still after subtracting 1 S>=0,The process continues; If minus 1 S<0,Indicates that there are no resources available and you need to block yourself and put it on the waiting queue. V Primitive: it means to release a resource, right S Atomic plus 1; If 1 is added S>0,The process continues; If 1 is added S<=0,Indicates that there are waiting processes on the waiting queue, and the first waiting process needs to be awakened.
Through the above explanation, mutex can use semaphores to block and evoke goroutine.
In fact, mutex is essentially a blocking operation about semaphores.
When goroutine cannot occupy the lock resource, it will be blocked and suspended. At this time, it cannot continue to execute the following code logic.
When mutex releases the lock resource, it will continue to evoke the previous goroutine to preempt the lock resource.
The state field of mutex is used for state flow. These state values involve some concepts, which we will explain in detail below.
- mutex status flag bit
The state of mutex has 32 bits, and its lower 3 bits respectively represent three states: wake-up state, lock state and hunger state. The remaining bits represent the number of goroutine s waiting for current blocking.
mutex will enter normal mode, starvation mode or spin mode according to the current state.
- mutex normal mode
When mutex calls the Unlock() method to release the lock resource, if there is a Goroutine queue waiting to be aroused, the Goroutine of the queue head will be aroused.
After the goroutine of the queue head is called, CAS method will be called to try to modify the state. If the modification is successful, it means that the lock resource is occupied successfully.
(Note: CAS is implemented by atomic.CompareAndSwapInt32(addr *int32, old, new int32) method in Go. CAS is similar to optimistic locking. Before modifying, it will first judge whether the address value is old or not. Only the old value will continue to be modified to new value, otherwise false will be returned, indicating that the modification fails.)
- mutex starvation mode
Since the Goroutine above does not directly occupy resources after being aroused, it is also necessary to call the CAS method to try to occupy lock resources. If there is a new Goroutine at this time, it will also call the CAS method to try to occupy resources.
However, for the Go scheduling mechanism, it will prefer the Goroutine with short CPU occupation time to run first, which will create a certain probability for the new Goroutine to obtain the lock resources. At this time, the Goroutine at the head of the team will not occupy it, resulting in starvation.
In response to this situation, Go adopted the hunger model. That is, by judging that the team leader Goroutine still cannot get the resources after a certain period of time, it will directly hand over the lock resources to the team leader Goroutine when Unlock releases the lock resources, and change the current state to starvation mode.
Later, if a new Goroutine is found to be in hunger mode, it will be directly added to the end of the waiting queue.
- mutex spin
If goroutine occupies lock resources for a short time, calling semaphores to block and evoke goroutine every time will be a waste of resources.
Therefore, after certain conditions are met, mutex will let the current Goroutine idle the CPU. After idling, it will call the CAS method again to try to occupy the lock resources until the spin conditions are not met, and it will eventually be added to the waiting queue.
The spin conditions are as follows:
It hasn't spun more than four times
Multi core processor
GOMAXPROCS > 1
The local Goroutine queue on p is empty
It can be seen that the spin condition is still relatively strict. After all, it will consume the computing power of the CPU.
- mutex's Lock() procedure
First, if mutex's state = 0, no one is occupying resources and blocking the goroutine waiting to be aroused. CAS method will be called to try to occupy the lock without other actions.
If m.state = 0 is not met, further judge whether spin is required.
When the spin is not needed or the resource is not available after the spin, the runtime is called_ Semacquiremutex semaphore function, which blocks the current goroutine and adds it to the waiting queue.
When the lock resource is released, after mutex evokes the goroutine of the team leader, the team leader goroutine will try to occupy the lock resource, and at this time, it may compete with the new goroutine.
When the team leader goroutine can't get the resources all the time, it will enter the hunger mode and directly give the lock resources to the team leader goroutine to block the new goroutine and add it to the tail of the waiting queue.
For starvation mode, it will not be released until the goroutine queue waiting for arousal is not blocked.
- Unlock procedure
mutex's Unlock() is relatively simple. Similarly, quick unlocking will be performed first, that is, there is no goroutine waiting to be aroused, so there is no need to continue to do other actions.
If the current mode is normal, simply evoke the team leader Goroutine. If it is in hunger mode, the lock will be directly handed over to the team leader Goroutine, and then the team leader Goroutine will be aroused to continue running.
mutex code details
Well, the above general flow has been completed. Next, we will present the detailed code flow so that you can know the logic of mutex's Lock() and Unlock() methods in more detail.
mutex Lock() code details:
// Lock mutex's lock method. func (m *Mutex) Lock() { // Quick lock if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { if race.Enabled { race.Acquire(unsafe.Pointer(m)) } return } // If fast locking fails, more locking actions will be performed. m.lockSlow() } func (m *Mutex) lockSlow() { var waitStartTime int64 // Record the waiting time of the current goroutine starving := false // Hungry awoke := false // Is it awakened iter := 0 // Spin number old := m.state // Current mutex status for { // The state of the current mutex is locked, is not in starvation mode, and meets the spin condition if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { // The wake-up flag has not been set yet if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) { awoke = true } runtime_doSpin() iter++ old = m.state continue } new := old // If not hungry, try locking // If it is hungry, it will not be locked because the current goroutine will be blocked and added to the end of the queue waiting to be aroused if old&mutexStarving == 0 { new |= mutexLocked } // Number of waiting queues + 1 if old&(mutexLocked|mutexStarving) != 0 { new += 1 << mutexWaiterShift } // If goroutine was in hunger mode before, it is also set to hunger mode this time if starving && old&mutexLocked != 0 { new |= mutexStarving } // if awoke { // If the status does not meet expectations, an error is reported if new&mutexWoken == 0 { throw("sync: inconsistent mutex state") } // The new status value needs to clear the wake-up flag because the current goroutine will be locked or sleep again new &^= mutexWoken } // CAS tries to modify the status. If the modification is successful, it indicates that lock resources have been obtained if atomic.CompareAndSwapInt32(&m.state, old, new) { // If the mode is not hungry and the lock has not been acquired, it means that the lock acquired this time is ok. return directly if old&(mutexLocked|mutexStarving) == 0 { break } // queueLifo is calculated based on the waiting time queueLifo := waitStartTime != 0 if waitStartTime == 0 { waitStartTime = runtime_nanotime() } // Here, it means that the locking is not successful // queueLife = true, goroutine will be placed at the head of the waiting queue // queueLife = false, goroutine will be placed at the end of the waiting queue runtime_SemacquireMutex(&m.sema, queueLifo, 1) // Calculate whether it conforms to the hunger mode, that is, whether the waiting time exceeds a certain time starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs old = m.state // Last time it was hunger mode if old&mutexStarving != 0 { if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 { throw("sync: inconsistent mutex state") } delta := int32(mutexLocked - 1<<mutexWaiterShift) // This is not a hunger mode, or there is no goroutine to evoke the waiting queue next time if !starving || old>>mutexWaiterShift == 1 { delta -= mutexStarving } atomic.AddInt32(&m.state, delta) break } // This is no longer the hunger mode. Clear the spin times and go back to the for loop to compete for the lock. awoke = true iter = 0 } else { old = m.state } } if race.Enabled { race.Acquire(unsafe.Pointer(m)) } }
mutex Unlock() code details:
// Unlock to unlock mutex // If the lock has not been locked, calling this method to unlock will throw a runtime error. // It will allow locking and unlocking on different Goroutine func (m *Mutex) Unlock() { if race.Enabled { _ = m.state race.Release(unsafe.Pointer(m)) } // Quick attempt to unlock new := atomic.AddInt32(&m.state, -mutexLocked) if new != 0 { // If quick unlocking fails, more unlocking actions will be performed. m.unlockSlow(new) } } func (m *Mutex) unlockSlow(new int32) { // In the unlocked state, an exception is thrown directly if (new+mutexLocked)&mutexLocked == 0 { throw("sync: unlock of unlocked mutex") } // Normal mode if new&mutexStarving == 0 { old := new for { // There are no waiting queues to evoke if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 { return } // Evoke waiting queue and number - 1 new = (old - 1<<mutexWaiterShift) | mutexWoken if atomic.CompareAndSwapInt32(&m.state, old, new) { runtime_Semrelease(&m.sema, false, 1) return } old = m.state } } else { //In the starvation mode, the lock is directly given to the queue head goroutine of the waiting queue runtime_Semrelease(&m.sema, true, 1) } }