Mutexes are an important topic in concurrency. They appear in almost every programming language when they talk about concurrency. In this post, we are going to look at what problems mutexes solve and how.
The race condition
The race condition appears when multiple goroutines try to access and update the shared data. They instead fail to update data properly and produce incorrect output. This condition is called race condition and happens due to repeated thread access.
Here is a simple program to illustrate the problem.
package main
import (
"fmt"
"sync"
)
func f(v *int, wg *sync.WaitGroup) {
*v++
wg.Done()
}
func main() {
var wg sync.WaitGroup
var v int = 0
for i := 0; i < 1000; i++ {
wg.Add(1)
go f(&v, &wg)
}
wg.Wait()
fmt.Println("Finished", v)
}
It is clear that the program is outputting the wrong result each time we run it. Here race condition is occurring. So, let’s try to rewrite this program with a mutex.
Working of a mutex in Golang
Before we write that program, let’s see what is mutex actually? A mutex is simply a mutual exclusion in short. It means that when multiple threads access and modify shared data before they do that they need to acquire a lock. Then when the work is done they release the lock and let some other goroutine to acquire the lock.
This allows the goroutines to have synchronized access to the data and prevents data race.
// working of a mutex:
// acquire lock
// modify data
// release lock
How to use the mutex in Go
Now, we will modify the above program so that the data race not occur. We will use a mutex to stop that. Here is an approach on how to go about it.
package main
import (
"fmt"
"sync"
)
func f(v *int, wg *sync.WaitGroup, m *sync.Mutex) {
// acquire lock
m.Lock()
// do operation
*v++
// release lock
m.Unlock()
wg.Done()
}
func main() {
var wg sync.WaitGroup
// declare mutex
var m sync.Mutex
var v int = 0
for i := 0; i < 1000; i++ {
wg.Add(1)
go f(&v, &wg, &m)
}
wg.Wait()
fmt.Println("Finished", v)
}
In this example after adding mutex, the program outputs correctly.
Mutex vs atomic package
It may be confusing that mutexes are very similar to atomic operations but they are much more complicated than that. Atomics utilize CPU instructions whereas mutexes utilize the locking mechanism. So when updating shared variables like integers, atomics are faster. But the real power of mutexes comes when the complex structure of data is handled concurrently. Then it is the only option since atomics don’t support that.