SoFunction
Updated on 2025-03-04

Summary of the implementation methods of concurrent control in Go

Concurrency control of Go

In actual Go development, concurrency security is a common thing, under concurrency,goroutineThere 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

channelChannels are mainly used ingoroutineMechanisms of communication and synchronization between. By usingchannel, can be found in differentgoroutineData is sent and received between them, so as to achieve coordination and control concurrency to achieve concurrency control.

according tochannelThe type of concurrency control can be achieved:

Unbuffered channel

When usingmakeWhen initializing, not specifiedchannelThe capacity size of the initialization without bufferingchannel

When the sending direction is not bufferedchannelWhen sending message data, ifchannelThe data of the receiver has not been retrieved by the receiver, then the currentgoroutineThis 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 createdch. Then in a separategoroutineA receive operation is initiated, waiting for the slave channelchReceive data in.

  • Next, inmain goroutineperform the send operation to the channelchSend data100

  • Due to the characteristics of unbuffered channels, when sending statementsch <- 100When executing, no receiver is ready to receive data (separatelygoroutineIn5sSleep), the sending operation will be blocked.

  • Receiver'sgoroutineWait until data is received.

  • When the recipientgoroutineAfter 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 unbufferedchannelWhen 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 usingmakeDuring initialization, you can specifychannelThe capacity size of the initialization is bufferedchannelThe capacity of a channel indicates the maximum number of elements that can be stored in the channel.

  • When the sender sends data to a cachechannelWhen, 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 bufferingchannelWhen receiving data, if the buffer is empty, the receiver will be blocked untilchannelThere is data to read;

Whether it is cachechannelStill no bufferingchannel, are all concurrently safe, that is, multiple goroutineData can be sent and received simultaneously without the need for an additional synchronization mechanism.

However, due to cachechannelIt 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、

existsyncIn the package,Can be concurrentgoroutineThe effect of executing barriers is played between them.WaitGroupProvides a code block that can wait for multiple concurrent executions to be reached when creating multiple goroutines.WaitGroupOnly after the specified synchronization conditions are displayed can the execution be continuedWaitsubsequent 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.goroutineProtected resources can be accessed, thereby avoiding data competition and uncertain results.

The functions of mutexes can be as follows:

  • Protecting shared resources: When multiplegoroutineWhen accessing shared resources concurrently, only one can be restricted by using mutex locks.goroutineShared 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 acquiredgoroutineOnly then can the code in this critical area be executed, othergoroutineYou 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, othergoroutineWill be blocked until nowgoroutineuntil the lock is released.Lock()Methods andUnlock()The underlying implementation principle is to use atomic operations to maintainMutexofstatestate.

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 multiplegoroutineFor global variablesxWhen self-increase, data overwrite will occur, so concurrent control can be achieved through some methods, such aschannelMutex lockAtomic operation

Can comparechannelandMutex lockExecution time when implementing concurrent control:

  • usechannel
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
}
  • useMutex
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 started10000indivualgoroutineimplement10000Sub-global variablesx++hour,channelImplement concurrent control of global variablesx++The execution time is6.2933ms(There is fluctuation), and useMutexThe 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 ischannelOperation involves **goroutineThe 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 involvegoroutinescheduling 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 multiplegoroutineRace conditions that may occur when accessing the same variable at the same time.

sync/atomicThe 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(&amp;x, 1)
    ()
}

func main() {
    for i := 0; i &lt; 10000; i++ {
       (1)
       go atomicAdd() // Atomic operation add function    }
    ()
    (x) // 10000
}

Some commonly used atomic operation functions:

  • Addfunction:AddInt32AddInt64AddUint32AddUint64etc. used to perform variablesAtomic addition operation
  • CompareAndSwapfunction:CompareAndSwapInt32CompareAndSwapInt64CompareAndSwapUint32CompareAndSwapUint64etc., used for comparison and exchange operations, assigning the new value to the specified address when the old value is equal to the given value.
  • Loadfunction:LoadInt32LoadInt64LoadUint32LoadUint64etc., used for loading operations, returning the value stored in the specified address.
  • Storefunction:StoreInt32StoreInt64StoreUint32StoreUint64etc., used for storing operations, storing the given value to a specified address.
  • Swapfunction:SwapInt32SwapInt64SwapUint32SwapUint64etc., 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!