How The Data Race Detector In Golang Works

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 concur­rently, 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

  1. 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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.