In this tutorial, we will learn about mutexes.We will also learn how to solve race conditions using mutexes and channels.
Mutex is a struct type
Critical section
Critical 在这里翻译为 临界的
it is important to understand the concept of critical section in concurrent programming.
When a program runs concurrently, the parts of code which modify shared resources should not be accessed by multiple Goroutines at the same time.
This section of code that modifies shared resources is called critical section.
For example, let’s assume that we have some piece of code that increments a variable x by 1.
x = x + 1
Let’s see why this code will fail when there are multiple Goroutines running concurrently. For the sake 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
- get the current value of x
- compute x + 1
- assign the computed value in step 2 to x
When these three steps are carried out by only one Goroutine, all is well.
Let’s discuss what happens when 2 Goroutines run this code concurrently. 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. Goroutine 1 gets the initial value of x, computes x + 1 and before it could assign the computed 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.
Tip
Pay attation to the system context switches
Now let’s 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 starts 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 output of the program depends on the sequence of execution of Goroutines is called race condition. 竞态
程序输出依赖于 sequence of execution of Goroutines is called
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 in time. This is made possible by using Mutex.
竞态可以避免,如果只有一个Goroutine访问临界的代码部分(公共的代码部分)。实现这个就要做“互斥”
Mutex
A Mutex is used to provide a locking mechanism to ensure that only one Goroutine is running the critical section of code at any point in time 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 condition.
mutex.Lock()
x = x + 1
mutex.Unlock()
In the above code, x = x + 1 will be executed by only one Goroutine at any point in time thus preventing race condition.
If one Goroutine already holds the lock and if a new Goroutine is trying to acquire a lock, the new Goroutine will be blocked until the mutex is unlocked.
Program with a 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)
}
Solving the race condition 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)
}
It is important to pass the address of the mutex in line no. 18. 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
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)
}
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 in line no. 10
Mutex vs Channels
In general use channels when Goroutines need to communicate with each other.
In the case of the problem which we solved above, I would prefer to use mutex since this problem does not require any communication between the goroutines. Hence mutex would be a natural fit.
My advice would be to choose the tool for the problem and do not try to fit the problem for the tool :)