SoFunction
Updated on 2025-03-06

Detailed explanation of concurrency security and locking issues in Go language

First, you can read this article first and have some understanding of locks

Detailed explanation of mutex locks and read and write locks for concurrent programming in GO language

Mutex-mutex

Mutex implementation mainly relies on CAS instructions + spin + semaphore

Data structure:

type Mutex struct {
	state int32
	sema  uint32
}

The above two structures that together occupy only 8 bytes of space represent the mutex lock in Go language

state:

By default, all status bits of the mutex are 0.int32Different bits in the same represent different states:

  • 1 bit indicates whether it is locked
  • 1 bit indicates whether a coroutine has been awakened
  • 1 bit indicates whether it is in a hungry state
  • The remaining 29 bits represent the number of blocking coroutines

Normal mode and hunger mode

Normal mode: All goroutines are locked in the order of FIFO, and the wake-up goroutine and the new request lock's goroutine are locked at the same time. Usually, the new request lock's goroutine is easier to acquire the lock (continuously occupy the CPU), while the wake-up goroutine is not easy to acquire the lock.

Hunger mode: All goroutines that try to acquire the lock are waiting to queue. The goroutines that request the lock will not be locked (spin disabled), but will be added to the tail of the queue to wait to acquire the lock.

If a Goroutine acquires a mutex and it is at the end of the queue or it waits for less than 1ms, the current mutex switches back to normal mode.

Compared with starvation mode, mutexes in normal mode can provide better performance, and starvation mode can avoid high tail delay caused by Goroutine being stuck in waiting and unable to acquire the lock.

Mutex locking process

  • If the mutex is in the initial state, the lock will be added directly
  • If the mutex is in locked state and works in normal mode, the goroutine will enter the spin, waiting for the lock to be released

The conditions for entering the spin are very harsh:

  • Mutex can only enter spin in normal mode;
  • runtime.sync_runtime_canSpinNeed to return true

Run on multi-CPU machines;

The current number of times the lock enters the spin is less than four times;

There is at least one running processor P on the current machine and the processed run queue is empty;

  • If the current Goroutine waits for locks exceeds 1ms, the mutex will switch to hunger mode;
  • The mutex lock will be connected under normal circumstancesruntime.sync_runtime_SemacquireMutexSwitch the Goroutine that attempts to acquire the lock to sleep, waiting for the lock holder to wake up;
  • If the current Goroutine is the last waiting coroutine on the mutex lock or the waiting time is less than 1ms, it switches the mutex back to normal mode;

Mutex unlocking process

When the mutex has been unlocked, an exception will be thrown after unlocking it.

When the mutex is in starvation mode, the ownership of the lock is handed over to the Goroutine at the front of the waiting queue.

When the mutex is in normal mode, if no Goroutine is waiting for the lock to be released or if a wake-up Goroutine has already obtained the lock, it will return directly; in other cases, it will wake up the corresponding Goroutine;

Regarding the use of mutex locks, it is recommended that you cannot use the same Mutex globally when writing services. Do not divide the locking and unlocking into more than two Goroutines for Mutex. Do not copy (including not being passed through function parameters), otherwise the status of the lock before the parameter is passed: locked or not locked. It is easy to generate deadlocks, the key is that the compiler cannot find this Deadlock~

RWMutex-Read and Write Lock

RWMutex in Go uses a writing-first design

Data structure:

type RWMutex struct {
	w           Mutex	//The capability provided by multiplexed mutex locks	writerSem   uint32	//writer semaphore	readerSem   uint32	//reader semaphore	readerCount int32	//Stores the number of read operations currently being performed	readerWait  int32	// Indicates the number of waiting for the read operation to complete when the write operation is blocked}

Write lock

Get the write lock:

  • Call the subsequent write operations of the Mutex structure held by the structure
  • WillreaderCountReduce 2^30 to become a negative number to block subsequent read operations
  • If other Goroutines hold read locks, the Goroutine will enter a dormant state and wait for all read locks to be released after execution.writerSemThe semaphore wakes up the current coroutine

Release the write lock:

  • WillreaderCountChange back to positive number, release the read lock
  • Wake up all the Goroutines that sleep because of the lock read
  • Call Release the write lock

When acquiring a write lock, the acquisition of the write lock will be blocked first, and then the acquisition of the read lock will be blocked. This strategy can ensure that the read operation will not be "starved" by continuous write operations.

Read lock

Get the read lock

How to get a read lockVery simple, this method willreaderCountAdd one:

  • If the method returns a negative number (indicates that other goroutines have obtained a write lock, the current goroutine will cause it to hibernate and wait for the lock to be released.
  • If the method returns a non-negative result, it means that no goroutine has obtained a write lock and will return successfully

Release the read lock

How to unlock the reading lock, this method will:

  • WillreaderCountDecrement one, and the return value will be processed separately.
  • If the return value is greater than or equal to 0, the read lock will be unlocked successfully
  • If it is less than 0, it means that there is a write operation being executed, it will be called,WillreaderWaitminus one, and trigger the semaphore after all read operations are releasedwriterSemWhen this semaphore is triggered, the scheduler will wake up the Goroutine that attempts to acquire the write lock.

WaitGroup

Can wait for a set of Goroutines to return

Three methods have been exposed to the outside world:

Method name Function
(wg * WaitGroup) Add(delta int) Counter + delta
(wg *WaitGroup) Done() Counter decrement by 1
(wg *WaitGroup) Wait() Block until the counter becomes 0

Just rightA simple encapsulation of the method is equivalent to adding -1

The map built in Go language is not concurrently safe.

Go languagesyncA concurrent secure version of map is available out of the box –. Use mutex locks to ensure concurrency security

Data structure:

type Map struct {
    mu Mutex
    read  // readOnly
    dirty map[interface{}]*entry
    misses int
}

Out of the box means that you can use it directly without using make function initialization like built-in map. at the same timeBuilt-in method:

Method name Function
(m *)Store(key, value interface{}) Save key-value pairs
(m *)Load(key interface{}) Get the corresponding value according to the key
(m *)Delete(key interface{}) Delete key-value pairs
(m *)Range(f func(key, value interface{}) bool) traversal. Range's argument is a function
* No Len( ) method

Atomic operation (atomic package)

The locking operation in the code will be time-consuming and costly because the context switching involving kernel state is involved. For basic data types, we can also use atomic operations to ensure concurrency security, because atomic operations are methods provided by Go and can be completed in the user state, so their performance is better than locking operations. Atomic operations in Go are provided by the built-in standard library sync/atomic.

References:

Go language concurrent programming, synchronous primitives and locks | Go language design and implementation ()

This is the end of this article about concurrent security and locks in Go language. For more related content in concurrent security and locks in Go language, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!