SoFunction
Updated on 2025-03-02

A brief discussion on the issue of safety of golang concurrent operation variables

I won't say much nonsense, let's just read the code~

package main 
import (
	"fmt"
	"time"
	"sync"
	"sync/atomic"
)
 
func main() {
	test1()
	test2()
}
 
func test1() {
	var wg 
	count := 0
	t := ()
	for i := 0 ; i < 50000 ; i++ {
		(1)
		go func(wg *,i int) {
			count++ //count is not concurrently safe			()
		}(&wg,i)
	}
 
	()
	(().Sub(t))
	("count====&gt;",count) //The value of count is <50000	("exit")
} 
 
func test2() {
	var wg 
	count := int64(0)
	t := ()
	for i := 0 ; i &lt; 50000 ; i++ {
		(1)
		go func(wg *,i int) {
			atomic.AddInt64(&amp;count,1) //Atomic Operation			()
		}(&amp;wg,i)
	}
 
	()
	(().Sub(t))
	("count====&gt;",count) //The value of count is 50000	("exit")
}

Execution results:

18.0485ms
count====> 46621
exit
16.0418ms
count====> 50000
exit

Supplement: golang based on shared variable concurrency

Concurrency definition: When we cannot confidently confirm that an event occurs before or after another event, it means that the two events x and y are concurrent.

Concurrency Security: If all its accessible methods and operations are concurrency-safe, then the type is concurrency-safe.

Race Condition: The program does not give the correct result when multiple goroutines perform operations across the cross.

As long as there is

Two goroutine concurrent access

The same variable, and

One of the fewer ones is that data competition will occur when writing operations.

Data race occurs when more than two goroutines concurrently access the same variable and at least one of them is a write operation.

The first type: Don’t write variables, variables are initialized in advance.

The second type: only one goroutine is allowed to access variables, and use select to listen to operations (go's golden sentence: do not communicate through shared variables, share variables through communication (channel).

The third type: allows a goroutine to access variables, but only one goroutine is allowed at the same time.

Now let's talk about the specific operation of the third situation

golang We can use the channel as a meter to ensure that there are several goroutines that can be accessed at the same time. make(chan struct{},1), locks access to the specified code block by blocking write and read.

var (
sema = make(chan struct{}, 1) // a binary semaphore guarding balance
balance int
)
func Deposit(amount int) {
sema <- struct{}{} // acquire token
balance = balance + amount
<-sema // release token
}
func Balance() int {
sema <- struct{}{} // acquire token
b := balance
<-sema // release token
return b
}

It can be guaranteed that there is only one goroutine to access at the same time.

However, we can use Mutex in the sync package to implement the above function, that is:

Mutex lock

Mutex: Ensure that shared variables will not be accessed concurrently.

import "sync"
var (
mu  // guards balance
balance int
)
func Deposit(amount int) {
()
balance = balance + amount
()
}
func Balance() int {
()
b := balance
()
return b
}

The contents of the code segment between Lock and Unlock can be read or modified at will, and this code segment is called the critical section.

Note: You must release the lock (Unlock). No matter what the situation is, you can use defer (). You must be sure to note that there is no reentrant lock in Go. If you encounter operations with smaller atoms, consider decomposing it into small block functions without lock function.

Next we will add another type of lock: read and write lock

In many cases, we need to ensure the read performance, and the mutex lock will temporarily prevent the operation of other goroutines and cannot achieve a good multi-concurrency effect (multiple read and single write). At this time, the read and write lock can solve this problem well.

RLock() and RUnlock() acquire and release a read or shared lock. RLock can only be available if there is no write operation for the critical section shared variable. Generally speaking, we should not assume that logical read-only functions/methods will not update certain variables. If you can't confirm, then use Mutex for a long time

Finally, let’s talk about the problem of memory synchronization

var x, y int
go func() {
x = 1 // A1
("y:", y, " ") // A2
}()
go func() {
y = 1 // B1
("x:", x, " ") // B2
}()

The above example: A1, A2, B1, B2 executes in sequence but is irregular

There may be a bunch of processors in modern computers, each with its local cache. For efficiency, writes to memory are generally buffered in each processor and flushed to main memory if necessary. In this case, these data may be submitted to main memory in a different order than the original goroutine write order. This causes the program to run serially, and the serial code accesses shared variables at the same time. Although it is necessary to observe that x=1 is successfully executed before y is read, it cannot ensure that it can observe and get the write to y in goroutine B, so A may also print out an old version of y.

There are two ways to solve it:

1. Variables are limited to use in goroutine and do not access shared variables

2. Access with mutually exclusive conditions

The above is personal experience. I hope you can give you a reference and I hope you can support me more. If there are any mistakes or no complete considerations, I would like to give you advice.