In previous articles, we detailed how Goroutine + channels in Go share memory through communication, enabling concurrent programming.

But Go also provides the traditional way to achieve concurrency by sharing variables, which is shared memory. This article will introduce Go

Relevant mechanisms provided.

1. What are races

After a Go program is run, there are many goroutines running at the same time, and the code in each goroutine is executed sequentially. If we cannot determine the order in which the code in the two Goroutines is executed. You can say that the two Goroutines are executed concurrently.

A piece of code can be said to be concurrency safe if the result is correct whether it is executed sequentially or concurrently.

Concurrency insecure code can cause a variety of problems, such as deadlocks, live locks, moralizations, and so on. Deadlocks and live locks indicate that the code can no longer execute, while races indicate that the code can execute, but with the possibility of incorrect results.

A typical example is making a deposit into a bank account:

var balance int
func Deposit(amount int) {
    balance = balance + amount
}
func Balance(a) int {
    return balance
}
Copy the code

Suppose two people now make 100 deposits in this account at the same time:

for i := 0; i < 100; i++ {
	go func(a) {
		Deposit(100)
	}()

	go func(a) {
		Deposit(100()})}// Sleep for a second to allow the goroutine execution to complete
time.Sleep(1 * time.Second)
fmt.Println(Balance())
Copy the code

If the program is correct, the final output should be 20000, but multiple runs could result in 19800, 19900, or some other value. At this point, we say that the program has a data race.

The root cause of this problem is that the balance = balance + amount line is not atomic on the CPU and may be interrupted halfway through execution.

2. How to eliminate races

When a race occurs, you have to resolve it. In general, there are three ways to resolve races:

  • Don’t modify variables

    If a variable does not need to be modified, it is safe to access it from anywhere, but this approach does not solve the above problem.

  • Do not access the same variable from multiple Goroutines

The Goroutine + channel we talked about earlier is one such idea, updating variables through channel blocking, which is consistent with the design philosophy of Go code: do not communicate through shared memory, but share memory through communication.

  • Only one Goroutine can access a variable at a time

If only one Goroutine can access the variable at any one time, the other Goruotine s have to wait until the current access is over before they can access it, which also eliminates the race. The following tools are used to ensure that only one Goroutine can access the variable at any one time.

3. Concurrency tools provided by Go

The following tools are used in Go to implement that only one Goroutine variable can be accessed at a time. Let’s take a look at each:

3.1 the mutex

This is the most classic tool to solve the problem of race. The principle is that if you want to access a resource, you must obtain the lock of the resource. Only when you obtain the lock can you access the resource, other goroutines must wait until the current Goroutine releases the lock and grabs the lock

Before using it, you need to apply a lock for the resource, using sync.Mutex, which is an implementation of the Mutex in the Go language:

var mu sync.Mutex
var balance int
Copy the code

Each Goroutine that takes a lock needs to ensure that it is released at the end of the variable access, even in exceptional cases, and we can use defer to ensure that the lock is finally released:

func Deposit(amount int) {
	mu.Lock()
	defer mu.Unlock()
	balance = balance + amount
}

func Balance(a) int {
	mu.Lock()
	defer mu.Unlock()
	return balance
}
Copy the code

After the code is changed, run the above deposit code again, no matter how many times, the final result is 20000. Here, we have solved the race problem, but there are still some small problems.

3.2 Read/write mutex

The mutexes above solve the problem of race access to data, but there is a small problem. The operation of reading the balance is a little inefficient, and every time the balance is read, the lock is still needed. In fact, if this variable is not changed, even if multiple Goroutines are reading at the same time, there is no concurrency problem.

Ideally, we would like to run multiple Goroutines reading simultaneously if the variable is not being written, which would be much more efficient.

Go also provides this tool, read-write locks. The lock reads and reads are not mutually exclusive. Simply put, this lock guarantees that only one Goroutine can write at a time. If one goroutine is writing, other Goroutines can neither read nor write, but multiple Goroutines can read at the same time.

Let’s change the above code again, with only one change:

var mu sync.RWMutex / / replace the sync. Mutex
var balance int
Copy the code

After this change, the above deposit code will still print 20000, but can allow multiple Goroutines to read the balance at the same time.

Most race problems in the Go language can be solved using these two tools.

3.3 Once

The Go language also provides a facility to ensure that code is executed only once, mostly for scenarios such as resource initialization. The way to use it is simple:

o := &sync.Once{}
for i := 0; i < 100; i++ {
	o.Do(func(a){
		go func(a) {
			Deposit(100)
		}()

		go func(a) {
			Deposit(100)} ()})}// Sleep for a second to allow the goroutine execution to complete
time.Sleep(1 * time.Second)
fmt.Println(Balance())
Copy the code

If the above code uses Once, it will only save Once, so the above code will always print 200.

3.4 Race detector

Many race errors are hard to find, and the Go language provides a tool to help check for races in your code. It’s easy to use, just add the -race argument after the following command:

$ go run -race

$ go build -race

$ go test -race
Copy the code

If one goroutine writes to a variable and nothing is synchronized, another goroutine reads or writes to the variable, then there is a race and an error is reported. For example:

data := 1

go func() {
	data = 2
}()

go func() {
	data = 3
}()

time.Sleep(2 * time.Second)
Copy the code

After running go run-race main.go, the following error is reported:

Found 1 data race(s)
exit status 66
Copy the code

4. Summary

Go also provides concurrent programming mechanisms provided by traditional languages, as well as shared memory. Go provides a relatively compact interface, but it provides a lot of power.

The text/Rayjun