Race Condition in Golang

Race Condition in Golang

Writing a concurrent code is often a difficult task. often programmers working with concurrent code, face similar kinds of bugs. One of them is inconsistency while manipulating the data using concurrent goroutines.

Let's see an example.

package main

import (
    "fmt"
    "sync"
)

var msg string
var wg sync.WaitGroup

func foo() {
    defer wg.Done()
    msg = "Hello Universe"
}

func bar() {
    defer wg.Done()
    msg = "Hello Cosmos"
}

func main() {

    msg = "Hello world"

    wg.Add(2)
    go foo()
    go bar()

    wg.Wait()
    fmt.Println(msg)

}

Note: If you are not familiar with Goroutines and WaitGroups, I recommend checking out our course on Concurrency in Golang

In the above code example, we are deploying two goroutines named foo and bar. Both goroutines are updating a variable named message to "Hello Universe" and "Hello Cosmos" accordingly. When we run this code, It will deploy the two goroutines. These goroutines eventually update the value of msg and join back to the main goroutine. Since two goroutines are updating the same variable in the above code, we cannot determine the output of the fmt.Println(msg), i.e, It will either print "Hello Universe" or "Hello Cosmos". Since we cannot determine the value stored in the msg variable, this will be a serious bug in our code that can break our software.

Let's see another example with a real-life scenario. Michael and his wife have a joint bank account, they use the same account to make purchases. One fine day, Michael went to have coffee at Starbucks 🥤🥤. Meanwhile, her wife went shopping and purchased a beautiful dress 👗👗 for herself. They paid their bills simultaneously, But the bank software was not implemented correctly which resulted in an inconsistency in their balance amount at the bank.

Let's have a look at the bank software's code and see what went wrong.

package main

import (
    "fmt"
    "sync"
)

var bankBalance int
var wg sync.WaitGroup

func Purchase(purchaseAmount int) {
    defer wg.Done()

    value := bankBalance
    value = value - purchaseAmount
    bankBalance = value
}

func main() {

    //initially Michael and his wife had $1000 USD in their bank account
    bankBalance = 1000

    //they made bill payment simultaneously
    wg.Add(2)

    go Purchase(5)
    go Purchase(157)

    wg.Wait()

    //final amount in there bank account
    fmt.Println("Final amount : ", bankBalance)
}

When we run the above code, It can result in 995, 838 or 843. Final values completely depend on the order of execution of the Purchase goroutine.

The above case is called Race condition in programming.

Race Condition

A race condition occurs when two or more concurrent goroutines try to access and modify the same data. For example, if one goroutine tries to read a variable, meanwhile other goroutines are trying to update the value of the same variable.

Race condition mostly occurs, if the developer thinks the concurrent program is executing sequentially. In the above example of bank software code, the developer might have assumed go Purchase(5) will complete its execution first than go Purchase(157).

How to detect race conditions in GO ?

Go provides built-in support to tackle the Race condition issue. We can use the -race flag while compiling or building our code to detect the race conditions in our program.

Let's run our code above with the -race flag to detect where the race condition is actually happening.

sonukumarsaw@legion-5PRO:concurrency$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x0000005d4400 by goroutine 7:
  main.Purchase()
      /home/sonukumarsaw/alpha/ioScript/concurrency/main.go:14 +0x74
  main.main·dwrap·2()
      /home/sonukumarsaw/alpha/ioScript/concurrency/main.go:27 +0x39

Previous write at 0x0000005d4400 by goroutine 8:
  main.Purchase()
      /home/sonukumarsaw/alpha/ioScript/concurrency/main.go:16 +0x8c
  main.main·dwrap·3()
      /home/sonukumarsaw/alpha/ioScript/concurrency/main.go:28 +0x39

Goroutine 7 (running) created at:
  main.main()
      /home/sonukumarsaw/alpha/ioScript/concurrency/main.go:27 +0x90

Goroutine 8 (finished) created at:
  main.main()
      /home/sonukumarsaw/alpha/ioScript/concurrency/main.go:28 +0xd2
==================
Final amount : 838
Found 1 data race(s)
exit status 66

Well, there's a lot of things going on here. Let's go through it step by step. First of all, we Found 1 data race. Now let's see the most important lines

Goroutine 7 (running) created at:
  main.main()
      /home/sonukumarsaw/alpha/ioScript/concurrency/main.go:27 +0x90

Goroutine 8 (finished) created at:
  main.main()
      /home/sonukumarsaw/alpha/ioScript/concurrency/main.go:28 +0xd2

Above two message shows, two goroutines are deployed named Goroutine 7 at line 27, i.e, go Purchase(5) and Goroutine 8 at line 28, i.e, go Purchase(157). Also, Goroutine 8 has already completed its execution while Goroutine 7 is still running.

Now let's see the next part.

Read at 0x0000005d4400 by goroutine 7:
  main.Purchase()
      /home/sonukumarsaw/alpha/ioScript/concurrency/main.go:14 +0x74
  main.main·dwrap·2()
      /home/sonukumarsaw/alpha/ioScript/concurrency/main.go:27 +0x39

Previous write at 0x0000005d4400 by goroutine 8:
  main.Purchase()
      /home/sonukumarsaw/alpha/ioScript/concurrency/main.go:16 +0x8c
  main.main·dwrap·3()
      /home/sonukumarsaw/alpha/ioScript/concurrency/main.go:28 +0x39

It clearly states that Read at 0x0000005d4400 by goroutine 7: and Previous write at 0x0000005d4400 by goroutine 8:. This means Goroutine 7 is trying to read the same memory address 0x0000005d4400 which was earlier written by Goroutine 8.

Even though, we could detect the race condition and its cause. We can still see the correct amount in the output i.e, Final amount : 838.

Let's make some changes in our code to make sure, our code returns the incorrect amount.

package main

import (
    "fmt"
    "sync"
)

var bankBalance int
var wg sync.WaitGroup

func Purchase(purchaseAmount int) {
    defer wg.Done()

    value := bankBalance
    time.Sleep(1 * time.Second)                 // some delay in processing the calculation
    value = value - purchaseAmount
    bankBalance = value
}


func main() {

    //initially Michael and his wife had $1000 USD in their bank account
    bankBalance = 1000

    //they made bill payment simultaneously
    wg.Add(2)

    go Purchase(5)
    go Purchase(157)

    wg.Wait()

    //final amount in there bank account
    fmt.Println("Final amount : ", bankBalance)
}

We have added time.Sleep() to imitate a processing delay. Now when we run this program, we will never get the correct amount in the output.

How to fix the race condition ?

To avoid race conditions in our program, any operation in a shared variable, must be executed atomically. Shared variables are those variables that are shared among the goroutines.

We can execute a program atomically by locking the critical section, a section of the code where shared variables are being manipulated. Once, the execution of the critical section is completed by one goroutine, other goroutines can acquire the lock and complete its execution as well.

We can use go's built-in Mutex lock. Mutex lock is a part of the sync package. We will see how to fix our code using Mutex lock in our next article.

Before You Leave

If you found this article valuable, you can support us by dropping a like and sharing this article with your friends.

You can sign up for our newsletter to get notified whenever we post awesome content on Golang.

Reference

Frame 1.png

Did you find this article valuable?

Support ioScript.org by becoming a sponsor. Any amount is appreciated!