This article is translated
Original address: golangbot.com/mutex/
A key part
Before we get into mutex, it’s important to understand the concept of a key part of concurrent programming. When a program is running concurrently, parts of the code that modify a shared resource should not be accessed by multiple Goroutines at the same time. This piece of code that modifies a shared resource is called the critical part. For example, suppose we have a piece of code that increments x by 1.
x = x + 1
Copy the code
As long as a single Goroutine accesses the above code, there should be no problem.
Let’s take a look at why this code fails when running multiple Goroutines simultaneously. For simplicity, let’s assume that there are two Goroutines running the above lines at the same time.
Internally, the above lines of code will be executed by the system in the following steps (there are more technical details about registers, how addition works, etc., but for the sake of this tutorial, we’ll assume three steps),
- I get the current value of x
- Let’s compute x plus 1
- Assign the calculated value in Step 2 to X
All is well when these three steps are performed through a Goroutine.
Let’s discuss what happens when two Goroutines run this code at the same time. The figure below illustrates what might happen if two Goroutines access code x = x + 1 at the same time.
Let’s assume that x starts at 0. Goroutine 1 gets the initial value of x, evaluates x + 1, and then switches the system context to Goroutine 2 before assigning the calculated value to X. Now Goroutine 2 gets its initial value x is still 0, so compute x + 1. After that, the system context switches to Goroutine 1 again. Now Goroutine 1 assigns its calculated value of 1 to X, so x becomes 1. Goroutine 2 then starts executing again and reassigns its calculated value to X, so x is 1 after both Goroutines.
Now let’s look at another scenario that could happen.
In the above case, Goroutine 1 starts and completes all three of its steps, so the value of x changes to 1. Then Goroutine 2 starts executing. X now has a value of 1, and x will have a value of 2 when Goroutine 2 completes execution.
So in both cases, you can see that the final value of x is either 1 or 2, depending on how the context switch occurs. The undesirable situation in which the output of a program depends on the order in which Goroutines are executed is called a race condition.
In these cases, contention conditions can be avoided if only one Goroutine is allowed to access a critical part of the code at any point in time. By using Mutex, you can do this.
Mutex
Mutex is used to provide a locking mechanism to ensure that only one Goroutine is running a critical part of the code at any one time to prevent a race situation.
Mutex is available in sync packages. Two methods are defined on Mutex, namely Lock and Unlock. Any code that exists between calls, Lock and Unlock will be executed by only one Goroutine, thus avoiding contention.
mutex.Lock()
x = x + 1
mutex.Unlock()
Copy the code
In the code above, x = x + 1 can only be executed by one Goroutine at any time to prevent a race situation.
If a Goroutine already holds the lock, and if a new Goroutine tries to acquire it, the new Goroutine is blocked until the mutex is unlocked.
There are competing procedures
In this section, we’ll write a program with race conditions, and in the next section, we’ll address race conditions.
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup) {
x = x + 1
wg.Done()
}
func main(a) {
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)
}
Copy the code
In the above program, the increment of the function x is incremented by 1, and then Done() is called to notify WaitGroup that it is complete.
We’ve generated 1,000 increment Goroutines. Each of these Goroutines runs at the same time, and a contention condition occurs when you try to increment x. Because multiple Goroutines are trying to access the value of x at the same time.
Run this program multiple times on your local computer, and you’ll find that the output is different each time due to race conditions. Some of the outputs I encountered were final value of x 941, final value of x 928, final value of x 922, etc.
Use mutex to resolve race conditions
In the above program, we generated 1000 Goroutines. If each increases the value of x by 1, then the final expected value of x is 1000. In this section, we will use mutex to fix race conditions in the above program.
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(a) {
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)
}
Copy the code
Mutex is a struct type, and we create a variable m of type Mutex with zero value. In the above program, we changed the increment function so that x. The increment code x = x + 1 is between m.lock () and m.lock (). Now, this code has no race conditions, because only one Goroutine is allowed to execute this code at any point in time.
Program output:
final value of x 1000
Copy the code
Passing the address of the mutex is important. If the mutex is passed by value rather than by address, each Goroutine will have its own copy of the mutex, and the race condition will still occur.
Use channels to resolve race conditions
We can also use channel channels to resolve race conditions. Let’s 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(a) {
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)
}
Copy the code
In the above program, we create a buffer channel of capacity 1 and pass this channel into the Goroutine of row increment. This buffer channel is used to ensure that only one Goroutine can access the code critical part of increment X. This is done by passing true to the buffer channel. Before x increases, it’s 8. Since the buffered channel has capacity 1, all other Goroutines attempting to write to the channel are blocked until the value is read from the channel after incrementing x. In practice, this allows only one Goroutine to access critical parts.
Program output:
final value of x 1000
Copy the code
Mutex contrast channel
We have solved the problem of race conditions using mutexes and channels. So how do we decide what to use and when? The answer lies in the problem you are trying to solve. If the problem you are trying to solve is more suited to mutex, keep using mutex. Don’t hesitate to use mutex if necessary. If the problem seems more appropriate for channels, use :).
Most Go novices try to solve every concurrency problem using channels, because it’s a cool feature of the language. This is wrong. The language gives us the option to use Mutex or Channel, and there’s nothing wrong with either.
In general, channels are used when goroutines need to communicate with each other, and mutex is used when only one Goroutine should access a critical part of the code.
In the case where we solved the problem above, I prefer to use mutex because this problem does not require any communication between goroutines. Therefore, mutex is a natural choice.
My advice is to pick the tool that solves the problem and not try to fit the problem to that tool 🙂