[Go learning notes] Chapter 13 Go concurrent resource competition

Preface: the following contents are compiled while reading [snow merciless] boss's blog. Some of the contents have been deleted and modified. It is recommended that you Go to the original author's blog to learn. The contents of this blog are only used as your own learning notes. Before that, I finished the introductory course of Go language with Mr. Han Ru at station b.

Learning links: https://www.flysnow.org/archives/
Reference book: Go language practice

13, Go concurrent resource competition

If there is concurrency, there is resource competition. If two or more goroutine s access a shared resource without synchronizing with each other, for example, when reading and writing to the resource at the same time, they will be in a competitive state. This is resource competition in concurrency.

Concurrency itself is not complicated, but the problem of resource competition makes it complicated for us to develop good concurrent programs, because it will cause many inexplicable problems.

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var (
	count int32
	wg    sync.WaitGroup
)

func main() {
	wg.Add(2)
	go incCount()
	go incCount()
	wg.Wait()
	fmt.Println(count)
}

func incCount() {
	defer wg.Done()
	for i := 0; i < 2; i++ {
		value := count
		runtime.Gosched()
		value++
		count = value
	}
}

This is an example of resource competition. We can run this program several times and find that the result may be 2, 3 or 4. Because the shared resource count variable does not have any synchronization protection, two goroutines will read and write it, which will overwrite the calculated results and produce wrong results. Here we demonstrate a possibility. The two goroutines are temporarily called g1 and g2.

  1. g1 read that the count is 0.
  2. Then g1 pauses, switches to g2, and g2 reads that the count is also 0.
  3. g2 pauses and switches to g1. g1 pairs count+1 and count becomes 1.
  4. g1 pauses and switches to g2. g2 has just obtained the value 0 and + 1. Finally, it is assigned to count or 1
  5. Have you noticed that the result of g1 for count+1 was overwritten by g2 just now? Are both goroutine s + 1 or 1

I won't continue the demonstration. The result here is wrong. The two goroutines cover each other's results. The runtime.Gosched() here means to suspend the current goroutine, return to the execution queue and let other waiting goroutines run. The purpose is to let us demonstrate that the result of resource competition is more obvious. Note that the CPU problem is also involved here. If multiple cores are parallel, the effect of resource competition is more obvious.

Therefore, the reading and writing of the same resource must be atomic, that is, only one goroutine can read and write shared resources at the same time.

The problem of shared resource competition is very complex and difficult to detect. Fortunately, Go provides us with a tool to help us check. This is the go build -race command. We execute this command in the current project directory to generate an executable file, and then run the executable file to see the printed detection information.

go build -race

An additional - race flag is added, so that the generated executable program has the function of detecting resource competition. Next, we run it on the terminal.

./hello

The executable file name generated by my example here is hello, so it runs like this. At this time, let's look at the detection results output by the terminal.

➜  hello ./hello       
==================
WARNING: DATA RACE
Read at 0x0000011a5118 by goroutine 7:
  main.incCount()
      /Users/xxx/code/go/src/flysnow.org/hello/main.go:25 +0x76

Previous write at 0x0000011a5118 by goroutine 6:
  main.incCount()
      /Users/xxx/code/go/src/flysnow.org/hello/main.go:28 +0x9a

Goroutine 7 (running) created at:
  main.main()
      /Users/xxx/code/go/src/flysnow.org/hello/main.go:17 +0x77

Goroutine 6 (finished) created at:
  main.main()
      /Users/xxx/code/go/src/flysnow.org/hello/main.go:16 +0x5f
==================
4
Found 1 data race(s)

Look, find a resource competition, and even if there is a problem in that line of code, it is marked. goroutine 7 reads the shared resource value: = count in line 25 of the code. At this time, goroutine 6 is modifying the shared resource count = value in line 28 of the code. Both goroutines are started from the main function and passed the go keyword in lines 16 and 17.

Since we already know that the problem of shared resource competition is that two or more goroutines read and write to them at the same time, we just need to ensure that only one goroutine can not read and write at the same time. Now let's look at the traditional solution to resource competition - locking resources.

Go language provides some functions in atomic package and sync package to synchronize shared resources. Let's take a look at atomic package first.

package main

import (
	"fmt"
	"runtime"
	"sync"
	"sync/atomic"
)

var (
	count int32
	wg    sync.WaitGroup
)

func main() {
	wg.Add(2)
	go incCount()
	go incCount()
	wg.Wait()
	fmt.Println(count)
}

func incCount() {
	defer wg.Done()
	for i := 0; i < 2; i++ {
		value := atomic.LoadInt32(&count)
		runtime.Gosched()
		value++
		atomic.StoreInt32(&count,value)
	}
}

Note that two functions, atomic.LoadInt32 and atomic.StoreInt32, read the value of int32 variables and modify the value of int32 variables. These two operations are atomic. Go has helped us use the locking mechanism at the bottom to ensure the synchronization and security of shared resources, so we can get correct results, At this time, we will use the resource competition detection tool go build race to check, and there will be no problem.

There are many atomized functions in the atomic package that can ensure the synchronous access and modification of resources under concurrency. For example, the function atomic.AddInt32 can directly modify a variable of int32 type. How many functions can be added on the basis of the original value is also atomic. There are no more examples here. You can try it yourself.

Although atomic can solve the problem of resource competition, it is relatively simple and supports limited data types. Therefore, Go language also provides a sync package. This sync package provides a mutually exclusive lock, which allows us to flexibly control which codes. At the same time, only one goroutine can access the code range controlled by the sync mutually exclusive lock, It is called critical area. The code of critical area can only be accessed by another goroutine at the same time. In that example, we can still transform it like this.

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var (
	count int32
	wg    sync.WaitGroup
	mutex sync.Mutex
)

func main() {
	wg.Add(2)
	go incCount()
	go incCount()
	wg.Wait()
	fmt.Println(count)
}

func incCount() {
	defer wg.Done()
	for i := 0; i < 2; i++ {
		mutex.Lock()
		value := count
		runtime.Gosched()
		value++
		count = value
		mutex.Unlock()
	}
}

In the example, a mutex mutex sync.Mutex is newly declared. The mutex has two methods, mutex.Lock() and mutex.Unlock(). The area between the two is the critical area, and the code in the critical area is safe.

In the example, we first call mutex.Lock() to lock the code with competing resources. In this way, when a goroutine enters this area, other goroutines cannot enter and can only wait until mutex.Unlock() is called to release the lock.

This method is flexible and allows the coder to arbitrarily define the code range to be protected, that is, the critical area. In addition to atomic functions and mutexes, Go also provides us with the function of easier synchronization in multiple goroutine s, which is channel chan, which will be discussed in the next article.

Tags: Go

Posted on Wed, 17 Nov 2021 00:47:33 -0500 by ksimpkins