In programs involving Goroutines, a data race occurs when two Goroutines access the same variable concurrently and at least one of the accesses is a write.
One of the typical examples of a data race is a
Let’s look at a program that counts the occurrences of each letter in a word through Goroutines.
This program involves a hash map that maintains the count by each letter. Due to the concurrent execution of the Goroutines, if we do not handle data race, the result will be erratic.
package mainimport ("fmt""math/rand""sync""time")var wg sync.WaitGroupfunc main() {fmt.Println("Hello, playground")myMap := make(map[string]int)for _, c := range "abcaaaaaaaabb" {wg.Add(1)go func(letter string, hm map[string]int, wg *sync.WaitGroup) {defer wg.Done()r := rand.Intn(10)time.Sleep(time.Duration(r) * time.Second)hm[letter] = hm[letter] + 1}(string(c), myMap, &wg)}wg.Wait()for k, v := range myMap {fmt.Println(k, v)}}
In the code above, there is a potential data race in line 23:
hm[letter] = hm[letter] + 1
This same hashmap is passed (by reference) across multiple Goroutines. This results in an error:
fatal error: concurrent map read and map write
In order to simulate data race, we have introduced a random time delay (in seconds) in line 22:
time.Sleep(time.Duration(r) * time.Second)
We can correct this behavior by forcefully serializing the updates to the shared variable. Let’s look at the below code to see how this is done.
package mainimport ("fmt""math/rand""sync""time")var wg sync.WaitGroupfunc main() {fmt.Println("Hello, playground")myMap := make(map[string]int)var mutex = &sync.Mutex{}for _, c := range "abcaaaaaaaabb" {wg.Add(1)go func(letter string, hm map[string]int, wg *sync.WaitGroup) {defer wg.Done()r := rand.Intn(10)time.Sleep(time.Duration(r) * time.Second)mutex.Lock()hm[letter] = hm[letter] + 1mutex.Unlock()}(string(c), myMap, &wg)}wg.Wait()for k, v := range myMap {fmt.Println(k, v)}}
Note that in the above code, we have fixed the data race by acquiring a
mutex.Lock()
hm[letter] = hm[letter] + 1
mutex.Unlock()
This is one of the common ways of protecting & serializing updates of a shared variable.
Sometimes, we encounter scenarios where we want to safely update a global counter across multiple Goroutines.
Use sync/atomic package and implement such atomic counters.
Below, we see an example of how to atomically update a global counter across multiple Goroutine executions.
package mainimport ("fmt""sync""sync/atomic")func main() {var counter uint64var wg sync.WaitGroupfor i := 0; i < 50000; i++ {wg.Add(1)go func() {atomic.AddUint64(&counter, 1)wg.Done()}()}wg.Wait()fmt.Println("counter:", counter)}
Free Resources