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.int32
Different 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_canSpin
Need to return trueRun 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 circumstances
runtime.sync_runtime_SemacquireMutex
Switch 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
- Will
readerCount
Reduce 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.
writerSem
The semaphore wakes up the current coroutine
Release the write lock:
- Will
readerCount
Change 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 will
readerCount
Add 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:
- Will
readerCount
Decrement 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
,Will
readerWait
minus one, and trigger the semaphore after all read operations are releasedwriterSem
When 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 right
A simple encapsulation of the method is equivalent to adding -1
The map built in Go language is not concurrently safe.
Go languagesync
A 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 |
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!