Welcome to tutorial no. 25 in Golang tutorial series.

In this tutorial, we will learn about mutexes. We will also learn how to solve race conditions using mutexes and channels.

Critical section

Before jumping to mutex, 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  

As long as the above piece of code is accessed by a single Goroutine, there shouldn't be any problem.

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(there are more technical details involving registers, how addition works, and so on but for the sake of this tutorial lets assume that these are the three steps),

  1. get the current value of x
  2. compute x + 1
  3. assign the computed value in step 2 to x

When these three steps are carried out by only one Goroutine, all is well.

x = x + 1
Goroutine 2Goroutine 2x0x + 11xx1

Now let's see a different scenario of what could happen.

Goroutine 11Goroutine 2xGoroutine 2x2
12

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.

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.

LockUnlock
mutex.Lock()  
x = x + 1  
mutex.Unlock()  
x = x + 1

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

In this section, we will write a program which has race condition and in the upcoming sections, we will fix the 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)
}
incrementx1Done()
increment
final value of x 941final value of x 928final value of x 922

Solving the race condition using a mutex

In the program above, 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)
}
mMutexincrementx = x + 1m.Lock()m.Unlock()

Now if this program is run, it will output

final value of x 1000  

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

We can solve the race condition using channels too. 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() {  
    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)
}
1incrementtruex1

This program also prints

final value of x 1000  

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 a 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 newbies try to solve every concurrency problem using a channel as it is a cool feature of the language. This is wrong. The language gives us the option to either use Mutex or Channel and there is no wrong in choosing either.

In general use channels when Goroutines need to communicate with each other and mutexes when only one Goroutine should access the critical section of code.

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 :)

This brings us to the end of this tutorial. Have a great day.