Diagram mutex inside Go Understanding programming language core implementation source code

Posted by Smicks on Mon, 23 Dec 2019 02:57:45 +0100

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 articles21 More articles www.sreguide.com

Topics: Programming