This article was originally posted at ioscript.org
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
Top comments (0)