Content written by author Rahul Shewale, who is currently employed at Josh Software.
As we know, Golang is a powerful programming language with built-in concurrency. We can concurrently execute a function with other functions by creating goroutine using go
keyword. When multiple goroutines share data or variables, we can face hard to predict race conditions.
In this blog, I am covering following points
- What is data race condition and how can it occur?
- How can we detect race conditions?
- Typical Data Races examples and how can we solve race conditions?
What is the data race condition?
A data race occurs when two goroutines access the same variable concurrently, and at least one of the accesses is performing a write operation.
Following is a basic example of race condition:
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup wg.Add(5) for i := 0; i < 5; i++ { go func() { fmt.Println(i) wg.Done() }() } wg.Wait() } }
In the above example, you must have noticed we have invoked 5 goroutines and access i
variable inside the goroutine, but here we faced data race condition because all goroutine read data from i
variable concurrently and at the same time for loop write a new value into i
variable.
Program OUTPUT:
5 5 5 5 5
How can we detect race conditions?
Now that we know what is a race condition, let’s dive into how to detect these conditions on your Golang project. So Golang provides built-in powerful race detector tools for checking the possible race conditions in program.
To use the built-in race detector you need to simply add -race flag to your go run command:$ go run -race main.go
This command finds a data race condition in the program if any and print error stack where race condition is occurring
Sample Output$ go run -race race_loop_counter.go
================== WARNING: DATA RACE Read at 0x00c0000a8020 by goroutine 7: main.main.func1() /home/-/goworkspace/src/example/race_loop_counter.go:13 +0x3c Previous write at 0x00c0000a8020 by main goroutine: main.main() /home/-/goworkspace/src/example/race_loop_counter.go:11 +0xfc Goroutine 7 (running) created at: main.main() /home/-/goworkspace/src/example/race_loop_counter.go:12 +0xd8 ================== ================== WARNING: DATA RACE Read at 0x00c0000a8020 by goroutine 6: main.main.func1() Goroutine 6 (running) created at: main.main() /home/-/goworkspace/src/example/race_loop_counter.go:12 +0xd8 ================== 2 2 4 5 5 Found 2 data race(s) exit status 66
How can we solve it?
Once you finally find race condition, you will be glad to know that Go offers multiple options to fix it.
Rob Pike has very aptly stated the following phrase. The solution to our problem lies in this simple statement
“Do not communicate by sharing a memory; instead, share memory by communicating.” -Rob Pike
- Use Channel for data sharing
Following is a simple program where the goroutine accesses a variable declared in main, increments the same and then closes the wait channel.
Meanwhile, the main thread also attempts to increment the same variable, waits for the channel to close and then prints the variable value.
However, here a race condition in generated between main and goroutine as they both are trying to increment the same variable.
Problem example: package main import "fmt" func main() { wait := make(chan int) n := 0 go func() { n++ close(wait) }() n++ <-wait fmt.Println(n) }
To solve the above problem we will use the channel.
Solution: package main import "fmt" func main() { ch := make(chan int) go func() { n := 0 n++ ch <- n }() n := <-ch n++ fmt.Println(n) }
Here the goroutine incrementing the variable and variable value pass through the channel to main function and when channel receives data then main perform next operation.
2) Use sync.Mutex
Following is a program to get the total number of even and odd numbers from an array of integers, numberCollection and store into a struct.
Following is a program to get the total number of even and odd numbers from an array of integers, numberCollection and store into a struct.
Problem example: package main import ( "fmt" "sync" ) type Counter struct { EvenCount int OddCount int } var c Counter func main() { numberCollection := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} fmt.Println("Start Goroutine") var wg sync.WaitGroup wg.Add(11) for _, number := range numberCollection { go setCounter(&wg, number) } wg.Wait() fmt.Printf("Total Event Number is %v and Odd Number is %v\n", c.EvenCount, c.OddCount) } func setCounter(wg *sync.WaitGroup, number int) { defer wg.Done() if number%2 == 0 { c.EvenCount++ return } c.OddCount++ }
Output:
Total Event Number is 5 and Odd Number is 6
If program is checked by race detector flag then we notice line c.EvenCount++
and line no 31 c.OddCount++
generate race condition because all goroutine writes data into struct object concurrently.
Solution:
To solve this problem, we can use sync.Mutex to lock access to the struct object as in the following example:
package main import ( "fmt" "sync" ) type Counter struct { EvenCount int OddCount int mux sync.Mutex } var c Counter func main() { numberCollection := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} fmt.Println("Start Goroutine") var wg sync.WaitGroup wg.Add(11) for _, number := range numberCollection { go setCounter(&wg, number) } wg.Wait() fmt.Printf("Total Event Number is %v and Odd Number is %v\n", c.EvenCount, c.OddCount) } func setCounter(wg *sync.WaitGroup, number int) { defer wg.Done() c.mux.Lock() defer c.mux.Unlock() if number%2 == 0 { c.EvenCount++ return } c.OddCount++ }
3) Making Copy of variable if Possible
Problem example: package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup wg.Add(5) for i := 0; i < 5; i++ { go func() { fmt.Printf("%v ", i) wg.Done() }() } wg.Wait() }
In the above problem, we can see five goroutines invoked in for loop and access value of i from the goroutine. Every Goroutine is called asynchronously and goes to wait state until the for loop is completed or any block operation is created.
After for loop execution is completed all goroutine will start execution and try to access i variable. This will result in a race condition.
For this problem, we can easily pass copy argument to goroutine and every goroutine gets a copy of the variable. As shown in the example, below we use argument j instead of accessing i from within goroutine.
Solution : package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup wg.Add(5) for i := 0; i < 5; i++ { go func(j int) { fmt.Printf("%v ", j) wg.Done() }(i) } wg.Wait() }
Conclusion:
The preferred way to handle concurrent data access in Go is to use a channel and use -race
flag for generating data race report, which helps to avoid a race condition.