Golang is known for its high concurrency and high performance. However, with concurrent use, the dreaded data race problem can occur. Once you have a Data Race problem, it can be one of those bugs that is hard to find and debug because you don’t know when it will happen.

Example data race state

Here is an example of a data race that occurs:

func main() {
	fmt.Println(getNumber())
}

func getNumber() int {
	var i int
	go func() {
		i = 5
	}()

	return i
}
Copy the code

In the above example, getNumber declares a variable I, and then sets I separately in Goroutine, while the program is returning I from the function. Since we don’t know whether Goroutine has finished modifying I, we do two things:

(1) Goroutine first completes the modification of the I value, and finally returns the I value set to 5; (2) The value of variable I is returned from the function, resulting in a default value of 0.Copy the code

Now, depending on which of these two operations completes first, the output will be either 0 (the default integer value) or 5.

That’s why it’s called a data unexpectedly state: the name derives from the return value getNumber depending on which operation (1) or (2) completes first.

Check that state

Go (starting with V1.1) has a built-in data race detector that you can use to pinpoint potential data race conditions.

Using it is as simple as -race adding flags to a normal Go command-line tool.

Run -race main.go Run -race main.go Run -race main.go build -race main.go test -race main.go

The core principle behind all avoiding race states is to prevent simultaneous read and write access to the same variable or memory location.

Ways to avoid race states

Once you finally discover annoying data races, you’ll be happy to know that Go offers many solutions. All of these solutions help ensure that if we are writing to a variable, access to that variable is blocked.

(1) WaitGroup wait

The most direct way to resolve the data race is to block read access until the write operation is complete, which can be used

Func getNumber() int {var I int // initialize a WaitGroup variable var wg sync.waitgroup // Add(1) Add(1) go func() {I = 5 wg.done () {wg.done ()}() Wg.wait () return I} Wait until wG.done is the same as the number of tasks added to the queue via WG.add, i.e. Wait until wG.wait () return I}Copy the code

(2) Wait with channel blocking

This approach is similar in principle to the last approach, except that we use channels instead of wait groups:

Func getNumber() int {var I int // Create a channel of type structure, Done := make(chan struct{}) go func() {I = 5 Done < -struct {}{}}() block until done <-done return I}Copy the code

If you want to call getNumber repeatedly, blocking inside the function is simple, but can cause trouble. The next approach follows a more flexible blocking approach.

(3) Return channel

Instead of blocking the function with a channel (2) above, we can return a channel through which we can push the result once we get it. Unlike the first two methods, this method does not do any blocking itself. Instead, it preserves the timing of blocking the calling code.

// create a channel c := make(chan int) go func() {// create a channel c := make(chan int) go func() {// Channel c < -5}()Copy the code

The result can then be retrieved from the channel in the calling code when needed:

Func main() {// The code is blocked until the value is fetched from the pushed return channel. In contrast to the previous method, block in the main function, not the function itself I := < -getNumberchan () fmt.println (I)}Copy the code

This approach is more flexible because it allows higher-level functionality to determine its own blocking and concurrency mechanisms, rather than treating the getNumber functionality as a synchronization functionality.

(4) Use mutex

The above three methods solve the problem that I can be read only after the write operation is completed. Here’s the thing: Regardless of the order of the read and write, it’s just that they can’t happen at the same time. For this scenario, consider using mutex:

// First, create a structure Type SafeNumber struct {val int m sync.mutex} func (I *SafeNumber) Get() int {// The 'Lock' method of the mutex blocks if it is already locked // if not, Then it blocks other calls until the 'Unlock' method is called; If not, other calls will be blocked until the mutex's Unlock method is called i.m.lock () // until the method returns, Return i.val} func (I *SafeNumber) Set(val int) {// Similar to getNumber above, Lock the I object until writing the value of 'i.val' is complete. I.m.lock () defer i.m.lock () i.val = val} func getNumber() int {// create an example of 'SafeNumber' I := &safenumber {} // Use "Set" and "Get" instead of the usual copy modification and read values, so that we can only read when the write is complete, and vice versa go func() {i.set (5)}() return i.set ()}Copy the code

Then, GetNumber can be used just like any other case. At first glance, this approach seems useless because we still can’t guarantee the value I.

When multiple write and read operations are mixed together, using Mutex Mutex ensures that the read and write values match the expected results.

conclusion

All of these methods prevent data race warnings when running a command with the -race flag. Each approach has its own trade-offs and complexities, so you need to weigh the pros and cons before using it.

In general, using WaitGroup can solve problems with a minimum of hassle, but you need to be careful that the Add and Done methods appear the same number of times, and that you call Wait to Wait for the added tasks to complete. If the numbers of Add and Done are inconsistent, the program will continue to block, consuming resources such as memory without limit, until the resources run out and the service crashes.

The core principle behind the above approaches to data race is to prevent simultaneous read and write access to the same variable or memory location.

reference

Talk about Data Race in Golang

Data Race in Golang (Continued)

Data races in Go (Golang) and their solutions