If you have any questions or suggestions, please contact us in time. My official account is “Brain fried Fish”, GitHub address: github.com/eddycjy.

Hello, I’m fried fish.

Recently in the Go concurrency related content, found that there are still a lot of details easy to make people confused, a careless into the pit, and may not be on the line after running some data to find, that is really too people collapse.

Today, I’m going to share a few examples, hoping that you can avoid these “pits” when coding.

Case a

Demo code

The first example is from @Birdhouse’s Geek Time share, with the following code:

func main() {
	count := 0
	wg := sync.WaitGroup{}
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < 100000; j++ {
				count++
			}
		}()
	}
	wg.Wait()

	fmt.Println(count)
}
Copy the code

Think about it, what is the value of the count variable in the final output? Is it a million?

The output

In the code above, we increment the goroutine through a for-loop and use sync.waitGroup to ensure that all goroutines are executed before the final output value is printed.

The final output is as follows:

// Run 638853 for the first time // run 654473 for the second time // Run 786193 for the third timeCopy the code

The output value is not constant, that is, the output is different each time, and rarely reaches the imagined million.

The analysis reason

The reason for this is that count++ is not an atomic operation and contains several actions in assembly, as follows:

MOVQ "".count(SB), AX 
LEAQ 1(AX), CX 
MOVQ CX, "".count(SB)
Copy the code

It is possible for multiple Goroutines to read count 1212 at the same time, increment each by one, and write it back.

At the same time, other goroutines may also read values when they increment, resulting in overwrites, an error that allows concurrent access to shared data.

Found the problem

This kind of competition problem can be analyzed and found through the Go Race Detector provided by the Go language:

$ go run -race main.go 
==================
WARNING: DATA RACE
Read at 0x00c0000c6008 by goroutine 13:
  main.main.func1()
      /Users/eddycjy/go-application/awesomeProject/main.go:28 +0x78

Previous write at 0x00c0000c6008 by goroutine 7:
  main.main.func1()
      /Users/eddycjy/go-application/awesomeProject/main.go:28 +0x91

Goroutine 13 (running) created at:
  main.main()
      /Users/eddycjy/go-application/awesomeProject/main.go:25 +0xe4

Goroutine 7 (running) created at:
  main.main()
      /Users/eddycjy/go-application/awesomeProject/main.go:25 +0xe4
==================
...
489194
Found 3 data race(s)
exit status 66
Copy the code

The compiler listens for access (read or write) to its memory address by detecting all memory access. Access to and operations on shared variables can be discovered at application runtime, and problems can be found and warnings printed.

One thing to note is that Go Run-race is run-time detection, not compile-time. And Race has a clear performance overhead, often ten times that of a normal program, so don’t be afraid to turn this configuration on in a production environment and roll it over.

Case 2

Demo code

The second example comes from sharing a fried fish in your brain. The code is as follows:

func main() {
	wg := sync.WaitGroup{}
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func(i int) {
			defer wg.Done()
			fmt.Println(i)
		}(i)
	}
	wg.Wait()
}
Copy the code

Think about it, what is the final output? Are they all 4’s? Is the output stable and ordered?

The output

In the code above, we loop through multiple Goroutines through a for-loop, passing variable I to the goroutine as a parameter, and finally printing variable I inside the Goroutine.

The final output is as follows:

// Output 0 1 2 4 3 for the first time // Output 4 0 1 2 3 for the second timeCopy the code

Obviously, the output values are unordered and unstable, and the value is not 4. Why is that?

The analysis reason

The reason for this is that even if all of the Goroutines are created, the Goroutine is not necessarily running.

By the time Goroutine actually executes the output, the variable I (value copy) may not be the value it was created with.

The whole program twist is essentially divided into multiple stages, that is, different timelines of each operation, which can be divided into:

  • Create first: for-loop creates a Goroutine.

  • Rescheduling: The coroutine goroutine starts scheduling execution.

  • Before execution: Starts executing the output in goroutine.

At the same time, goroutine scheduling has a certain randomness (it is recommended to know the GMP model), so its output is bound to be disordered and unstable.

Found the problem

At this point, you might wonder if the aforementioned Go Run-race would have found this problem. As follows:

$ go run -race main.go
0
1
2
3
4
Copy the code

Obviously not without a warning, because it’s not an error per se to access shared data concurrently, and it causes the program to become serial, blinding you.

Case 3

Demo code

The third example comes from sharing a fried fish in a dream with the following code:

func main() {
	wg := sync.WaitGroup{}
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func() {
			defer wg.Done()
			fmt.Println(i)
		}()
	}
	wg.Wait()
}
Copy the code

Think about it, what is the final output? Are they all 4’s? Is it going to run around like case two?

The output

In the code above, there is no difference from case 2 except that the variable I is not passed as a parameter.

The final output is as follows:

// First output 5 5 5 5 5 5Copy the code

The initial output results are all 5, at this time some people will be confused, why not 4?

Many people will be confused by the fact that it’s not a 4, but don’t let one or two outputs fool you into thinking it’s a 5. You can output a few more times, as follows:

// Output several times 5 3 5 5 5 5Copy the code

Eventually you’ll find that… The output is random, not 100% of the output is 5, let alone 4. Why is that?

The analysis reason

The reason for this is actually very close to case 2, and if you understand case 2, you can solve case 3 in theory.

The essence of this is that creating a Goroutine is not synchronized with actually executing fmt.println. So it’s quite possible that by the time you execute fmt.println, the for-loop will have already run, so that the value of variable I ends up being 5.

So conversely, it’s possible that it doesn’t run out, that there’s randomness. Write a test case to see the obvious difference.

conclusion

In this article, I’ve shared a few of the most frequently seen concurrent “pits” recently, hoping to help you. Also, do you have any problems writing Go concurrent programs?

Also, do you have any problems writing Go concurrent programs?

Welcome to discuss and exchange.

My official account

Share Go language, micro service architecture and strange system design, welcome to pay attention to my public number and I exchange and communication.

The best relationship is mutual achievement. Your praise is the biggest motivation for the creation of fried fish. Thank you for your support.