Mutex in Go
In this tutorial, we will discuss Mutex in the Go language and learn how to solve race conditions using mutexes and channels.
Critical section
Before jumping to a mutex, it is important to understand the concept of the critical section in concurrent programming.
Critical Section is the part of a program that tries to access shared resources. That resource may be any resource in a computer like a memory location, Data structure, CPU, or any IO device.
For example lets assume that we have some piece of code which increments a variable x by 1.
x = x + 1
As long as a single Goroutine accesses the above code, there shouldn’t be any problem. Let’s see why this code will fail when multiple Goroutines are running concurrently.
For the principle of simplicity, let’s assume that we have 2 Goroutines running the above line of code concurrently.
Internally the above line of code will be executed by the system in the following steps(there are more technical details involving registers)
- Get the current value of x
- Compute x + 1
- Assign the computed value in step 2 to x
So when only one Goroutine carries out these three steps, all is well. Let’s discuss what happens when 2 Goroutines run this code concurrently.
Scenario 1
The picture below depicts one scenario of what could happen when two Goroutines access the line of code x = x + 1 concurrently.
We have assumed the initial value of x to be 0. Now Goroutine 1 gets the initial value of x, computes x + 1, and before assigning the calculated value to x, the system context switches to Goroutine 2.
Now Goroutine 2 gets the initial value of x, which is still 0, computes x + 1. After this, the system context switches again to Goroutine 1. Now Goroutine 1 assigns its computed value 1 to x, and hence x becomes 1.
Then Goroutine 2 starts execution again and then assigns its computed value, which is again 1 to x, and hence x is 1 after both Goroutines execute.
Scenario 2
Now lets see a different scenario of what could happen.
In the above scenario, Goroutine 1 starts execution and finishes all its three steps, and hence the value of x becomes 1. Then Goroutine 2 begins execution. Now the value of x is 1 and when Goroutine 2 finishes execution, the value of x is 2.
So from the two cases, you can see that the final value of x is 1 or 2 depending on how context switching happens. This type of undesirable situation where the program’s output depends on the sequence of execution of Goroutines is called a race condition.
In the above scenario, the race condition could have been avoided if only one Goroutine was allowed to access the critical section of the code at any point of time. This is made possible by using Mutex.
Mutex
A Mutex is a method used as a locking mechanism to ensure that only one Goroutine is accessing the critical section of code at any point of time. This is done to prevent race conditions from happening.
Mutex is available in the sync package. There are two methods defined on Mutex, namely Lock and Unlock.
Any code that is present between a call to Lock and Unlock will be executed by only one Goroutine, thus avoiding race conditions.
mutex.Lock()
x = x + 1 // this statement be executed by only one Goroutine at any point of time
mutex.Unlock()
In the above code, x = x + 1 will be executed by only one Goroutine at any point of time, thus preventing race conditions.
If one Goroutine already has the lock and if a new Goroutine is trying to get the lock, then the new Goroutine will be stopped until the mutex is unlocked.
To understand this concept, let’s first understand a program having race conditions.
Program with Race Condition
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup) {
x = x + 1
wg.Done()
}
func main() {
var w sync.WaitGroup
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w)
}
w.Wait()
fmt.Println("final value of x", x)
}
In the program above, the increment function increments the value of x by 1 and then calls Done() on the WaitGroup to notify its completion.
We spawn 1000 increment Goroutines from the above program. Each of these Goroutines run concurrently and race condition occurs when trying to increment x and here multiple Goroutines try to access the value of x concurrently.
Please run this program in your local as the playground is deterministic, and the race condition will not occur in the playground.
Run this program multiple times in your local machine, and you can see that the output will be different for each time because of race conditions.
Some of the outputs which I encountered are the final value of x 941, the final value of x 948, the final value of x 952, and so on.
Solving the race condition using mutex
In the above program, we spawn 1000 Goroutines. If each increments the value of x by 1, the final desired value of x should be 1000. In this section, we will fix the race condition in the program above using a mutex.
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
m.Lock()
x = x + 1
m.Unlock()
wg.Done()
}
func main() {
var w sync.WaitGroup
var m sync.Mutex
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, &m)
}
w.Wait()
fmt.Println("final value of x", x)
}
Output
final value of x 1000
Mutex is a struct type, and we create a zero-valued variable m of type Mutex. In the above program, we have changed the increment function so that the code which increments x x = x + 1 is between m.Lock() and m.Unlock().
Now, this code is void of any race conditions since only one Goroutine can execute this piece of code at any point of time.
Please note that it is important to pass the address of the mutex. If the mutex is passed by value instead of passing the address, each Goroutine will have its own copy of the mutex, and the race condition will still occur.
Solving the race condition using channel
We can solve the race condition using channels too. Lets see how this is done.
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, ch chan bool) {
ch <- true
x = x + 1
<-ch
wg.Done()
}
func main() {
var w sync.WaitGroup
ch := make(chan bool, 1)
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, ch)
}
w.Wait()
fmt.Println("final value of x", x)
}
Output
final value of x 1000
In the program above, we have created a buffered channel of capacity 1, and this is passed to the increment Goroutine.
This buffered channel ensures that only one Goroutine access the critical section of code which increments x. This is done by passing true to the buffered channel just before x is incremented.
Since the buffered channel has a capacity of 1, all other Goroutines trying to write to this channel are blocked until the value is read from this channel after incrementing x. Effectively this allows only one Goroutine to access the critical section.
Mutex vs Channels
We have solved the race condition problem using both mutexes and channels. So how do we decide what to use when? The answer lies in the problem you are trying to solve.
If the problem you are trying to solve is a better fit for mutexes, then go ahead and use mutex. Do not hesitate to use mutex if needed. If the problem seems to be a better fit for channels, then use it.
Most Go beginners try to solve every concurrency problem using a channel, as it is a cool language feature. This doesn’t seem right. The language gives us the option to either use Mutex or Channel, and there is no wrong in choosing either.
That’s all about the Mutex in Go language. If you have any queries or feedback, please write us email at contact@waytoeasylearn.com. Enjoy learning, Enjoy Go language.!!