This article was originally posted at ioscript.org
While writing concurrent code in Golang, often programs face situations when two or more goroutines try to access and modify shared variables. Such conditions can lead to inconsistency in data stored in the shared variable. Such conditions are called Race conditions. You can learn more about race conditions from our previous article of this course, Race conditions in Golang.
Let's see one example of Race Condition:
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
var counter int
fmt.Println("Initial value: ", counter)
// deploy 5 goroutines
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
//increment the counter 100 times
for j := 0; j < 100; j++ {
temp := counter
time.Sleep(time.Microsecond * 1)
temp += 1
counter = temp
}
}()
}
wg.Wait()
fmt.Println("Final value: ", counter)
}
Output
Initial value: 0
Final value: 102
As we can see in the output, there's definitely an inconsistency in our counter variable. The expected value of the counter is 500
and we go the 102
. Let's find out where is the issue in the above code causing the Race condition. To find the race condition in our code, we can use the command go run -race main.go
.
When we run/build our code with the -race
flag on. It generates the following output.
Initial value: 0
==================
WARNING: DATA RACE
Read at 0x00c000126008 by goroutine 8:
main.main.func1()
ioScript/concurrency/main.go:25 +0xa8
Previous write at 0x00c000126008 by goroutine 7:
main.main.func1()
ioScript/concurrency/main.go:28 +0xce
Goroutine 8 (running) created at:
main.main()
ioScript/concurrency/main.go:21 +0xdb
Goroutine 7 (running) created at:
main.main()
ioScript/concurrency/main.go:21 +0xdb
==================
Final value: 101
Found 1 data race(s)
exit status 66
We can see in the logs that go runtime found 1 data race. A race condition was found due to lines 25 and 28. There is a read operation in line 25 and a write operation at 28. Goroutine 8 is reading from the counter
variable and Goroutine 7 is trying to write to the counter variable.
We can solve this race condition if we somehow restrict the read and write operation of the shared variable to one goroutine at a time.
sync.Mutex
Mutex is a synchronization primitive which restricts one or more goroutine to enter the critical section while another goroutine is executing the critical section.
When one goroutine is executing the critical section of the code, mutex locks the section. Other goroutines need to wait till the execution is complete and the lock is released by the first goroutine.
Let's try to fix our above code using mutex lock.
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
var counter int
var mu sync.Mutex
fmt.Println("Initial value: ", counter)
// deploy 5 goroutines
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
//increment the counter 100 times
for j := 0; j < 100; j++ {
mu.Lock()
temp := counter
time.Sleep(time.Microsecond * 1)
temp += 1
counter = temp
mu.Unlock()
}
}()
}
wg.Wait()
fmt.Println("Final value: ", counter)
}
Output
ioscript@ioscript:concurrency$ go run -race main.go
Initial value: 0
Final value: 500
So, let's break down what we have done here. We have declared a variable name mu
which is of type Mutex provided by the sync package of golang. Mutex type provides two methods: Lock() and Unlock().
- Lock( ): As the name suggests, it locks the mutex. If the lock is already in use, other goroutine blocks until the mutex is available.
- Unlock( ): It unlocks the mutex. It throws a runtime error if the mutex is not locked when unlock was called.
In our code above, we have used mu.Lock()
just before we are reading a value from the counter variable. Then we are incrementing the value and write the value to the counter variable again. Since read-write operations are complete, we are releasing the lock on mutex using mu.Unlock( )
so that other goroutines can acquire the lock again and execute the critical section.
When we run this code with the -race
flag, we don't get any error message and the correct output is displayed in the code.
Conclusion
In this tutorial, we had a quick look into Race conditions and how they can create data inconsistency in our program. We then learn about Mutex Locks and how we can use them to solve race conditions in our code and ensure that our program always returns expected results.
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 find more articles in this series here
You can sign up for our newsletter to get notified whenever we post awesome content on Golang.
You can also let us know what you would like to read next? Drop a comment or email @ sonukumarsaw@ioscript.org
Reference
Top comments (0)