Go interview questions: Lock implementation principle sync-mutex

In Go, two main types of locks are implemented: sync.Mutex (mutex lock) and sync.RWMutex (read-write lock).

This article mainly introduces the use and implementation principles of sync.Mutex.

why lock is needed

Under high concurrency or when multiple goroutines are executed at the same time, the same memory may be read and written at the same time, such as the following scenario:

var count int
var mu sync.Mutex

func func1() {
    
    
	for i := 0; i < 1000; i++ {
    
    
		go func() {
    
    
			count = count + 1
		}()
	}
	time.Sleep(time.Second)
	fmt.Println(count)
}

The output value is expected to be 1000, but the actual value is 948, 965, etc. The results of multiple runs are inconsistent.
The reason why this phenomenon occurs is because for count=count+1, the execution steps of each goroutine are:

  • Read the current count value
  • count+1
  • Modify count value

When multiple goroutines are executed to modify the value at the same time, the goroutine executed later will overwrite the modification of count by the previous goroutine.

In Go, the most commonly used method to restrict access to public resources by concurrent programs is the mutex lock (sync.mutex).

There are two common methods of sync.mutex:

  • Mutex.lock() is used to obtain the lock
  • Mutex.Unlock() is used to release the lock.
    The code section between the Lock and Unlock methods is called the critical section of the resource. The code in this section is strictly protected by locks and is thread-safe. At any time, at most There is a goroutine executing.

Based on this, the above example can be improved using sync.mutex:

var count int
var mutex sync.Mutex

func func2() {
    
    
	for i := 0; i < 1000; i++ {
    
    
		go func() {
    
    
			mutex.Lock()
			count = count + 1
			mutex.Unlock()
		}()
	}
	time.Sleep(time.Second)
	fmt.Println(count)
}

The output result is 1000.

When a goroutine executes the mutex.lock() method, if other goroutines perform locking operations, they will be blocked. Other goroutines will not continue to grab the lock until the current goroutine executes the mutex.unlock() method to release the lock. lock execution.

Implementation principle

Data structure of sync.Mutex
The structure of sync.Mutex in Go is:

type Mutex struct {
    
    
	state int32
	sema  uint32
}

Sync.Mutex consists of two fields, state is used to indicate the current state of the mutex lock, and sema is used to control the semaphore of the lock status. I believe that after reading the descriptions of these two fields, all Taoists may seem to understand them, but they may not. Let's understand in detail what these two fields do.
The mutex lock state mainly records the following four states:

waiter_num : records the number of goroutines currently waiting to grab this lock.
starving : whether the current lock is in a starvation state (the starvation state of unlocking will be detailed later) 0: normal state 1: starvation state
wokeen : whether a goroutine in the current lock has been awakened. 0: No goroutine is awakened; 1: There is a goroutine in the process of locking.
Locked : Whether the current lock is held by the goroutine. 0: Not held 1: Already held
The role of the sema semaphore :
When the gorouine holding the lock releases the lock, the sema semaphore will be released. This semaphore will wake up the gorouine that was previously blocked by grabbing the lock to acquire the lock.

Two modes of lock

Mutex locks are designed in two main modes: normal mode and starvation mode.

The reason why starvation mode is introduced is to ensure the fairness of goroutine acquisition of mutex locks. The so-called fairness actually means that when multiple goroutines acquire locks, it is fair if the order in which the goroutines acquire the locks is consistent with the order in which the locks are requested.
In normal mode, all goroutines blocked in the waiting queue will acquire locks in order. When a goroutine in the waiting queue is awakened, this goroutine will not directly acquire the lock, but will compete with the new goroutine requesting the lock. Usually, it is easier for a goroutine that newly requests a lock to acquire the lock. This is because the goroutine that newly requests a lock is occupying the CPU slice for execution, and there is a high probability that the logic for acquiring the lock can be directly executed.

In starvation mode , the goroutine that newly requests a lock will not acquire the lock, but will join the end of the queue and block waiting to acquire the lock.
Trigger conditions for hunger mode :

  • When a goroutine waits for the lock for more than 1ms, the mutex lock will switch to starvation mode.

Conditions for canceling hunger mode:

  • When the goroutine that acquires the lock is the last goroutine in the queue waiting for the lock, the mutex lock will switch to normal mode.
  • When the waiting time of the goroutine that acquires the lock is within 1ms, the mutex lock will switch to normal mode.

Precautions

  1. After successfully executing Lock() in a goroutine, do not lock again, otherwise it will panic.
  2. Executing Unlock() before Lock() to release the lock will panic.
  3. For the same lock, you can execute Lock in one goroutine. After successfully locking, you can execute Unlock in another goroutine to release the lock.

Guess you like

Origin blog.csdn.net/m0_73728511/article/details/133011077