What is mutex? How does it prevent race conditions?
By Mobin Mohanan, R &D Engineer (Ethereum), Kerala Blockchain Academy
Mutual Exclusion, or simply “mutex”, is a concurrent programming feature to prevent race conditions.
A race condition occurs when two or more threads access shared data simultaneously, and at least one of the accesses is a write.
By locking the resource, a mutex ensures that only one thread can modify the resource at a time, thereby preventing data inconsistencies and ensuring thread safety.
Let’s write an example in Go.
Go uses “goroutines”, lightweight threads for concurrency.
We can write a goroutine by adding the “go” keyword before a function.
go f(x, y, z) {}
A “WaitGroup” waits for a collection of goroutines to finish. The main goroutine calls “Add” to set the number of goroutines to wait for. Then each of the goroutines runs and calls “Done” when finished. At the same time, “Wait” can be used to block until all goroutines have finished.
var wg sync.WaitGroup
wg.Add(int)
wg.Done()
wg.Wait()
First, we will create a faulty program without mutex displaying the need for it.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
counter := 0
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 10; j++ {
time.Sleep(time.Nanosecond)
counter++
}
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
The main function initialises a counter and a WaitGroup, then starts 10 goroutines. Each goroutine increments the counter 10 times, with a brief sleep between increments.
Theoretically, this program should print 100 as the counter value nonetheless. But sometimes, it won’t. (Try it yourself.)
That’s because some increments overlap, while one succeeds and the others fail, just like a “race”.
This issue increases “exponentially” if we increase the loop range or the number of goroutines.
This is where mutex comes in. In Go, it’s straightforward to implement mutex.
Declare a “Mutex” variable; the “Lock” method is used to acquire the mutex, blocking other goroutines from accessing the locked section until the mutex is released. The “Unlock” method releases the mutex, allowing other waiting goroutines to proceed.
var mu sync.Mutex
mu.Lock()
mu.Unlock()
Let’s rewrite our previous example and try it out.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
counter := 0
var wg sync.WaitGroup
var mu sync.Mutex
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 10; j++ {
time.Sleep(time.Nanosecond)
mu.Lock()
counter++
mu.Unlock()
}
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
See the difference?
Understanding the use of concurrency tools like goroutines, WaitGroups, and mutexes in Go is essential for writing efficient and safe concurrent programs.
By leveraging these mechanisms, you can manage multiple tasks simultaneously while preventing race conditions and ensuring proper synchronisation.
Mastering these concepts will enhance your ability to build robust and performant applications as you delve deeper into programming.