1. Basic concepts of locks
1.1 CAS and Polling
1.1.1 cas implements locks
CAS is increasingly used in lock implementations to acquire locks by exchanging values for a given variable using the processor's CAS instructions
1.1.2 Polling Lock
Thread CAS failures are most likely in multithreaded concurrent scenarios, and a polling attempt to retrieve locks is usually made in conjunction with a for loop
1.2 Lock Fairness
Locks are generally divided into fair locks and unfair locks in terms of fairness, depending on whether the thread that acquired the lock first acquired the lock prior to the subsequent thread or, if so, fair locks: multiple threads acquired the lock in the order in which they acquired the lock, otherwise, unfair
1.3 Hunger and Queuing
1.3.1 Lock Hunger
Lock hunger means that because a large number of threads are acquiring locks at the same time, some threads may fail during the lock's CAS process and fail to acquire locks for a long time
1.3.2 Queuing Mechanism
When CAS and polling locks are mentioned above for lock acquisition, you can see that if there are already threads that have acquired locks, but when the current thread fails to acquire locks in multiple polls, there is no need to continue and repeatedly attempt to waste system resources. A queuing mechanism is usually used to queue
1.4 Bit Count
In most programming languages, when a CAS-based lock is implemented, a 32-bit integer is usually used to store the lock state.
2. mutex implementation
2.1 Member Variables and Modes
2.1.1 Member Variables
The core member variable in go's mutex has only two states and sema, which count locks by state and queue by SEMA
type Mutex struct { state int32 sema uint32 }
2.1.2 Lock Mode
There are two main lock modes
describe | Fairness | |
---|---|---|
Normal mode | In normal mode, all goroutines acquire locks in FIFO order, and both waked goroutines and new requested goroutines acquire locks at the same time. It is usually easier for new requested goroutines to acquire locks | no |
Hunger mode | Hunger mode All goroutines attempting to acquire locks are queued. The goroutines requesting new locks do not acquire locks, but join the tail of the queue and wait to acquire locks. | yes |
As you can see above, in normal mode, the performance of locks is the highest. If multiple goroutine s acquire locks and release them immediately, queue consumption for multiple threads can be avoided. Similarly, after switching to hunger mode, when acquiring locks, if certain conditions are met, the locks will also be switched back to normal mode, thus ensuring high lock performance
2.2 Lock Count
2.2.1 Lock Status
Locks in mutex have three flags, of which the binary bits are 001(mutexLocked), 010(mutexWoken), and 100(mutexStarving), and note that they are not mutually exclusive. For example, a lock state may be locked hunger mode and has been awakened
mutexLocked = 1 << iota // mutex is locked mutexWoken mutexStarving
2.2.2 Wait Count
Three states of the current mutex are stored in the mutex by three bits lower, and the remaining 29 bits are all used to store the number of goroutine s trying to acquire a lock
mutexWaiterShift = iota // 3
2.3 Wake-up Mechanism
2.3.1 Wake-up Sign
The wake-up flag is actually the second one mentioned above. The wake-up flag is mainly used to identify if the goroutine you are trying to acquire is currently awakening. Remember that in the above fair mode, the goroutine currently running on the cpu may acquire a lock first
2.3.2 Wake-up Process
When a lock is released, if a goroutine is currently awakening, only modifying the lock state to release the lock will allow the goroutine in woken state to acquire the lock directly, otherwise it will need to wake up a goroutine and wait for the goroutine to modify the state to mutexWoken before exiting
2.4 Locking Process
2.3.1 Quick Mode
If there is no goroutine lock currently, and CAS succeeds directly, the direct lock acquisition succeeds
// Fast path: grab unlocked mutex. if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { if race.Enabled { race.Acquire(unsafe.Pointer(m)) } return }
2.3.2 Spin and Wake-up
// Notice that there are actually two pieces of information here: one is that if the current lock state is already in place, then allowing spin iter s is essentially counting times and actually only allowing spins four times // It's just spinning and waiting for someone to release the lock. If someone releases the lock, they immediately try the following logic to get it if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { // ! awoke if the current thread is not awake // Old&mutexWoken == 0 If there are currently no other waking nodes, the current node will be in the waking state // Old > > mutexWaiterShift!= 0: Move 3 bits to the right, if no bits are 0, indicating that there is currently a goroutine waiting // Atomic.CompareAndSwapInt32 (&m.state, old, old|mutexWoken) sets the current state to wake up if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) { awoke = true } // Trying to spin, runtime_doSpin() // Spin Count iter++ // Get Status From New old = m.state continue }
2.3.3 Change Lock Status
There are two possibilities for the process to come here: 1. Lock state is no longer locked 2. The spin exceeds the specified number of times and is no longer allowed to spin
new := old if old&mutexStarving == 0 { // If you're not currently hungry, you can actually try to acquire a lock here |=It's actually setting the bit of the lock to 1 to indicate the lock state new |= mutexLocked } if old&(mutexLocked|mutexStarving) != 0 { // If currently locked or hungry, increase the wait count new += 1 << mutexWaiterShift } if starving && old&mutexLocked != 0 { // If you are currently hungry and the current lock is still occupied, try switching the hunger mode new |= mutexStarving } if awoke { if new&mutexWoken == 0 { throw("sync: inconsistent mutex state") } // awoke true indicates that the mutexWoken state was modified successfully while the current thread was spinning on it // Clear Wake-up Flag Bits // Why clear the marker? // In fact, because it is very likely that the current thread will be suspended by subsequent processes, you will need to wait for another goroutine to release the lock to wake up // However, if mutexWoken's position is not 0 when unlock is found, the thread will not wake up and lock new &^= mutexWoken }
2.3.3 Locked Queuing and State Transition
Only one goroutine actually succeeds in locking the CAS while the other threads need to recalculate the spin and wake states above to recalculate the CAS again
if atomic.CompareAndSwapInt32(&m.state, old, new) { if old&(mutexLocked|mutexStarving) == 0 { // If the original state equals zero, the lock is now released and not in starvation mode // The actual binary bit may be like 1111000, with all three of the last three zeros, and only the counter that records waiting for the goroutine may not be zero // That means it's true break // locked the mutex with CAS } // Queuing logic, if waitStatrTime is found to be non-zero, then the current thread has been queued before, possibly because // unlock was awakened, but the lock was not acquired this time, so it was moved to the head of the waiting queue queueLifo := waitStartTime != 0 if waitStartTime == 0 { waitStartTime = runtime_nanotime() } // There is a queue waiting for other nodes to wake up runtime_SemacquireMutex(&m.sema, queueLifo) // Switch to starving=true if waiting longer than specified // If a thread was not hungry before and did not exceed starvationThresholdNs, starving is false // This triggers the following state transition starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs // Get State Again old = m.state if old&mutexStarving != 0 { // If you find that you are currently starving, notice that starvation wakes up the first goroutine // All goroutine s are currently queued // Consistency check, if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 { throw("sync: inconsistent mutex state") } // Get the current mode delta := int32(mutexLocked - 1<<mutexWaiterShift) if !starving || old>>mutexWaiterShift == 1 { // If the current goroutine is not hungry, switching from hungry mode to normal mode // Switch out of mutexStarving state delta -= mutexStarving } // Last cas operation atomic.AddInt32(&m.state, delta) break } // Reset Count awoke = true iter = 0 } else { old = m.state }
2.5 Release Lock Logic
2.5.1 Release Lock Code
func (m *Mutex) Unlock() { if race.Enabled { _ = m.state race.Release(unsafe.Pointer(m)) } // cas operation directly new := atomic.AddInt32(&m.state, -mutexLocked) if (new+mutexLocked)&mutexLocked == 0 { throw("sync: unlock of unlocked mutex") } if new&mutexStarving == 0 { // If the lock is released and not in hunger mode old := new for { if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 { // If you already have a waiter and are awakened, return directly return } // Subtract a wait count and switch the current mode to mutexWoken new = (old - 1<<mutexWaiterShift) | mutexWoken if atomic.CompareAndSwapInt32(&m.state, old, new) { // Wake up a goroutine runtime_Semrelease(&m.sema, false) return } old = m.state } } else { // Wake up waiting threads runtime_Semrelease(&m.sema, true) } }
Pay attention to bulletin numbers and read more source analysis articles More articles www.sreguide.com