Concurrency control of Go
In actual Go development, concurrency security is a common thing, under concurrency,goroutine
There is competition between data resources and other aspects.
forEnsure data consistency, prevent deadlocks, etc.As the problem arises, some methods need to be used to implement concurrency control in concurrency.
The purpose of concurrent control isEnsure that access and operations of shared resources can be carried out correctly and effectively in multiple concurrent threads or processes, and avoid the problems of race conditions and data inconsistencies.。
In Go, concurrent control can be achieved in the following ways:
1、channel
channel
Channels are mainly used ingoroutine
Mechanisms of communication and synchronization between. By usingchannel
, can be found in differentgoroutine
Data is sent and received between them, so as to achieve coordination and control concurrency to achieve concurrency control.
according tochannel
The type of concurrency control can be achieved:
Unbuffered channel
When usingmake
When initializing, not specifiedchannel
The capacity size of the initialization without bufferingchannel
;
When the sending direction is not bufferedchannel
When sending message data, ifchannel
The data of the receiver has not been retrieved by the receiver, then the currentgoroutine
This will block in the send statement until a recipient is ready to receive data, that is, the unbuffered channel requires that the sending operation and the receiving operation be prepared at the same time before the communication can be completed. Doing so ensures synchronization of sending and receiving, avoiding data competition and uncertainty.
package main import ( "fmt" "time" ) func main() { // Create an unbuffered channel ch := make(chan int) // Start a goroutine to receive data go func() { ( * 5) ("Waiting for data to be received") data := <-ch // Receive data ("Data received:", data) }() ("Send data") // Send data. Since the anonymous function goroutine sleeps, the data in the unbuffered channel has no goroutine received, so it will block. After 5 seconds, it will continue to execute ch <- 100 () ("Program End") }
In the above code, an unbuffered channel is created
ch
. Then in a separategoroutine
A receive operation is initiated, waiting for the slave channelch
Receive data in.Next, in
main goroutine
perform the send operation to the channelch
Send data100
。Due to the characteristics of unbuffered channels, when sending statements
ch <- 100
When executing, no receiver is ready to receive data (separatelygoroutine
In5s
Sleep), the sending operation will be blocked.Receiver's
goroutine
Wait until data is received.When the recipient
goroutine
After the preparation is made, the sending operation is completed, the data is successfully sent and received by the receiver, and the program then continues to execute subsequent statements and prints out the corresponding output.
It should be noted thatUsing unbufferedchannel
When there is no receiver, the sending operation will be permanently blocked, which may result in deadlocks., so when using unbuffered channels, it is necessary to ensure that the sending and receiving operations can match.
Buffered channel
When usingmake
During initialization, you can specifychannel
The capacity size of the initialization is bufferedchannel
,The capacity of a channel indicates the maximum number of elements that can be stored in the channel.。
When the sender sends data to a cache
channel
When, if the buffer is full, the sender will be blocked until there is a buffer space to receive the message data;When the receiver has buffering
channel
When receiving data, if the buffer is empty, the receiver will be blocked untilchannel
There is data to read;
Whether it is cachechannel
Still no bufferingchannel
, are all concurrently safe, that is, multiple goroutine
Data can be sent and received simultaneously without the need for an additional synchronization mechanism.
However, due to cachechannel
It has a cache space, so you need to pay special attention to the size of the cache space when using it to avoid excessive memory consumption or deadlocking.
2、
existsync
In the package,Can be concurrent
goroutine
The effect of executing barriers is played between them.WaitGroup
Provides a code block that can wait for multiple concurrent executions to be reached when creating multiple goroutines.WaitGroup
Only after the specified synchronization conditions are displayed can the execution be continuedWait
subsequent code. In useImplement synchronous mode to achieve concurrent control effect.
In Go,Types provide the following methods:
Method name | Function description |
---|---|
func (wg * WaitGroup) Add(delta int) |
Wait for group counter + delta |
(wg *WaitGroup) Done() |
Waiting for group counter-1 |
(wg *WaitGroup) Wait() |
Block until waiting for the group counter to become 0 |
Example:
package main import ( "fmt" "sync" ) // Declare global waiting group variablesvar wg func printHello() { ("Hello World") () // After completing a task, call the Done() method, wait for the group to be reduced by 1, and inform the current goroutine that the task has been completed} func main() { (1) // Wait for the group to add 1, which means registering a goroutine go printHello() ("main") () // Block the current goroutine until all goroutines in the group are waiting for the task to be completed} // Execution resultsmain Hello World
3、
It is a mutex in Go language (
Mutex
) type, used to implement mutually exclusive access to shared resources.
Mutex is a common concurrency control mechanism that ensures that there is only one at the same time.goroutine
Protected resources can be accessed, thereby avoiding data competition and uncertain results.
The functions of mutexes can be as follows:
- Protecting shared resources: When multiple
goroutine
When accessing shared resources concurrently, only one can be restricted by using mutex locks.goroutine
Shared resources can be accessed to avoid the problems of race conditions and data inconsistencies. - Implementing the critical area: a mutex can mark a piece of code as a critical area, only the lock has been acquired
goroutine
Only then can the code in this critical area be executed, othergoroutine
You need to wait for unlocking to access the code blocks in the critical area.
The basic way to use mutex locks is to call themLock()
The method acquires the lock, executes the critical area code, and then callsUnlock()
Method to release the lock. After obtaining the lock, othergoroutine
Will be blocked until nowgoroutine
until the lock is released.Lock()
Methods andUnlock()
The underlying implementation principle is to use atomic operations to maintainMutex
ofstate
state.
In addition to the most basic mutex lock, it also provides read and write locks. In scenarios where more reads and fewer writes, the performance of mutex locks can be improved compared to mutex locks.
channel and Mutex comparative examples
In self-increase operationx++
In this operation, this operation is not an atomic operation, so it is in multiplegoroutine
For global variablesx
When self-increase, data overwrite will occur, so concurrent control can be achieved through some methods, such aschannel
、Mutex lock
、Atomic operation
。
Can comparechannel
andMutex lock
Execution time when implementing concurrent control:
- use
channel
package main import ( "fmt" "sync" "time" ) var x int64 var wg func main() { startTime := () ch := make(chan struct{}, 1) for i := 0; i < 10000; i++ { (1) go func() { defer () ch <- struct{}{} x++ <-ch }() } () endTime := () (x) // 10000 ((startTime)) // 6.2933ms }
- use
Mutex
package main import ( "fmt" "sync" "time" ) var x int64 var wg var lock func main() { startTime := () for i := 0; i < 10000; i++ { (1) go func() { defer () () x++ () }() } () endTime := () (x) // 10000 ((startTime)) // 3.0835ms }
The execution time of the two methods can be compared and started10000
indivualgoroutine
implement10000
Sub-global variablesx++
hour,channel
Implement concurrent control of global variablesx++
The execution time is6.2933ms
(There is fluctuation), and useMutex
The provided mutex lock implements concurrent control of global variablesx++
The execution time is3.0835ms
(There is fluctuation), about twice as much. Why is this?
The reason ischannel
Operation involves **goroutine
The scheduling and context switching between **, while the mutex lock is under the layer of Go, the execution time is shorter, because the operation of the mutex lock is relatively lightweight and does not involvegoroutine
scheduling and context switching.
During the development process, choosing to use a channel or a mutex lock depends on the specific scenario and requirements. It does not necessarily mean that using a lock is enough. It needs to be selected based on the actual business scenario. If finer granular control and higher concurrency performance are required, mutex locks can be prioritized.
4. atomic atomic operation
Go provides atomic operations for synchronous access to variables in memory, avoiding multiplegoroutine
Race conditions that may occur when accessing the same variable at the same time.
sync/atomic
The package provides a series of atomic operations, comparison and exchange methods, which utilize the underlying atomic instructions to ensure atomic access and modification of variables in memory, thereby achieving concurrent control.
package main import ( "fmt" "sync" "sync/atomic" ) var x int64 var wg // Use atomic operationsfunc atomicAdd() { atomic.AddInt64(&x, 1) () } func main() { for i := 0; i < 10000; i++ { (1) go atomicAdd() // Atomic operation add function } () (x) // 10000 }
Some commonly used atomic operation functions:
-
Add
function:AddInt32
、AddInt64
、AddUint32
、AddUint64
etc. used to perform variablesAtomic addition operation。 -
CompareAndSwap
function:CompareAndSwapInt32
、CompareAndSwapInt64
、CompareAndSwapUint32
、CompareAndSwapUint64
etc., used for comparison and exchange operations, assigning the new value to the specified address when the old value is equal to the given value. -
Load
function:LoadInt32
、LoadInt64
、LoadUint32
、LoadUint64
etc., used for loading operations, returning the value stored in the specified address. -
Store
function:StoreInt32
、StoreInt64
、StoreUint32
、StoreUint64
etc., used for storing operations, storing the given value to a specified address. -
Swap
function:SwapInt32
、SwapInt64
、SwapUint32
、SwapUint64
etc., used for exchange operations, exchange the value stored in the specified address and the given value, and return the original value.
The above is the detailed summary of the implementation method of concurrent control in Go. For more information about the implementation of Go concurrent control, please pay attention to my other related articles!