Golang Mutex Tutorial [Lock() & Unlock() Examples]


GO

Reviewer: Deepak Prasad

Introduction to golang Mutex

In this tutorial, we are going to explore Go mutex, what mutex are ?, what problem mutex solves and how does it solve the problem.

Sometimes goroutines run simultaneously  and access and modify data at the same time. The goroutines start racing against each other trying to access and modify the same resource. This creates a situation in software engineering called race condition. Race condition occurs when more than one goroutine tries to access and modify the same data in memory address. This will lead to undesirable changes to the resources. Mutex in Go tries to solve this problem, where multiple goroutines share data and interact with the data simultaneously.

In simple terms, Go mutex ensures that only one goroutine can access the shared data at time to prevent race conditions. Mutex provides mutual exclusion to data and this means that only one goroutine can hold the data at a time and at the same time, any other goroutine that would want to access the data will have to acquire the mutex. Below are the main areas we will learn.

  1. Race condition example
  2. Locking and Unlocking with Mutex
  3. Using sync.RWMutex
  4. Using mutex with struct type

Enough talking, let's get our hands dirty and start using Go mutex. Mutex is a data structure that is provided by the Go sync package as sync.Mutex. The sync.Mutex comes with two methods named Lock() and Unlock().

Syntax

var m sync.Mutex
m.Lock()
// Block of code
m.Unlock()

Here,

  • var m sync.Mutex : Declares a variable m of type Mutex. This variable will be used to access the Lock() and Unlock() methods.
  • m.Lock() : Locks the mutex so that only one goroutine can access the data. This blocks other goroutines and they have to wait until the mutex is released.
  • m.Unlock() : Unlocks the mutex and exposes it to other goroutines that would like to access the data.

 

Race condition example

In this section , we are creating a situation similar to how a bank works. Users with a shared bank account may want to deposit and withdraw money at the same time. This will create race conditions in our code as shown below.

package main
 
import (
   "fmt"
   "math/rand"
   "sync"
   "time"
)
 
var BALANCE float32 = 12000
 
type User struct {
   name           string
   depositAmount  float32
   withdrawAmount float32
}
 
func (u *User) deposit(wg *sync.WaitGroup) {
   fmt.Printf("%s is depositing  USD %f \n", u.name, u.depositAmount)
   BALANCE += u.depositAmount
   wg.Done()
}
 
func (u *User) withdraw(wg *sync.WaitGroup) {
   fmt.Printf("%s is withdrawing  USD %f \n", u.name, u.withdrawAmount)
   BALANCE -= u.withdrawAmount
   wg.Done()
}
 
func main() {
 
   var wg sync.WaitGroup
   users := []User{
       {name: "Marco Lazerri", withdrawAmount: 1300, depositAmount: 1000},
       {name: "Paige Wilunda", withdrawAmount: 1400, depositAmount: 123},
       {name: "Gerry Riveres", withdrawAmount: 900, depositAmount: 25},
       {name: "Sean Bold", withdrawAmount: 200, depositAmount: 5432},
       {name: "Mike Wegner", withdrawAmount: 5600, depositAmount: 2344},
   }
   rand.Seed(time.Now().UnixNano())
   for i := range users {
       wg.Add(2)
       i = rand.Intn(len(users))
       go users[i].deposit(&wg)
       go users[i].withdraw(&wg)
       time.Sleep(time.Second)
 
   }
 
   wg.Wait()
 
   fmt.Printf("New account BALANCE is %f \n", BALANCE)
 
}

Explanation

In the above example, we create a User struct with name , depositAmount and depositeAmount of type string , float32 and float32 respectively. We also initialize a global variable BALANCE of type float32.  The User struct has methods namely deposit() and withdraw() that perform addition and subtraction to the global BALANCE variable. These two methods take an argument wg of type sync.WaitGroup, which will be used to ensure that each method waits for the other to complete executing using wg.Done().

In the main function, we initialize a wg variable of type sync.WaitGroup. The variable will be used by the main thread to wait for all goroutines to finish executing using the wg.Wait() call. We also initialize an array of users []User{}, then generate a random number to be used in the for loop to perform deposit() and withdraw() actions on random users. After each iteration the thread sleeps for a second to illustrate an expensive execution.

After all is done, we print the final balance on the terminal. One thing to note here is how we run our code e.g go run -race main.go. To be able for the code to panic with a race condition we need to add a -race flag to tell the compiler that we are interested in seeing the number of race conditions. Otherwise , the code if ran as go run main.go , no race condition will be shown.

Output

$ go run -race main.go
Mike Wegner is withdrawing  USD 5600.000000
Mike Wegner is depositing  USD 2344.000000
==================
WARNING: DATA RACE
Read at 0x00000058644c by goroutine 7:

Mike Wegner is depositing  USD 2344.000000
Mike Wegner is withdrawing  USD 5600.000000
Paige Wilunda is depositing  USD 123.000000
Paige Wilunda is withdrawing  USD 1400.000000
Paige Wilunda is depositing  USD 123.000000
Paige Wilunda is withdrawing  USD 1400.000000
Gerry Riveres is withdrawing  USD 900.000000
Gerry Riveres is depositing  USD 25.000000
New account BALANCE is 2059.000000
Found 1 data race(s)
exit status 66
 
Found 1 data race(s)
exit status 66

In the above output, the top most section warns about an impending race condition with WARNING: DATA RACE.  The bottom section indicates the number of race conditions found with warming found 1 data race(s).

 

Locking and Unlocking Mutex

In this section, we will identify a section in our code that is critical , lock it and unlock it when we need to.

Example

package main
 
import (
   "fmt"
   "math/rand"
   "sync"
   "time"
)
 
type User struct {
   name           string
   depositAmount  float32
   withdrawAmount float32
}
 
var BALANCE float32 = 12000
 
func (u *User) deposit(wg *sync.WaitGroup, mx *sync.Mutex) {
   fmt.Printf("%s is depositing  USD %f \n", u.name, u.depositAmount)
   mx.Lock()
   BALANCE += u.depositAmount
   defer mx.Unlock()
   wg.Done()
}
 
func (u *User) withdraw(wg *sync.WaitGroup, mx *sync.Mutex) {
   fmt.Printf("%s is withdrawing  USD %f \n", u.name, u.withdrawAmount)
   mx.Lock()
   BALANCE -= u.withdrawAmount
   defer mx.Unlock()
   wg.Done()
}
 
func main() {
 
   var wg sync.WaitGroup
   var mu sync.Mutex
   users := []User{
       {name: "Marco Lazerri", withdrawAmount: 1300, depositAmount: 1000},
       {name: "Paige Wilunda", withdrawAmount: 1400, depositAmount: 123},
       {name: "Gerry Riveres", withdrawAmount: 900, depositAmount: 25},
       {name: "Sean Bold", withdrawAmount: 200, depositAmount: 5432},
       {name: "Mike Wegner", withdrawAmount: 5600, depositAmount: 2344},
   }
   rand.Seed(time.Now().UnixNano())
   for i := range users {
       wg.Add(2)
       i = rand.Intn(len(users))
       go users[i].deposit(&wg, &mu)
       go users[i].withdraw(&wg, &mu)
       time.Sleep(time.Second)
   }
 
   wg.Wait()
 
   fmt.Printf("New account BALANCE is %f \n", BALANCE)
 
}

Explanation

We now lock the critical section of our code using mu.Lock() when goroutines race to access and modify the BALANCE variable. This prevents deposit() and withdraw() methods from accessing and modifying the BALANCE variable until the mutex is released after calling the mu.Unlock() method. In the output the no race conditions are returned and the program completes running successfully.

Output

$ go run -race main.go
Gerry Riveres is withdrawing  USD 900.000000
Gerry Riveres is depositing  USD 25.000000
Paige Wilunda is depositing  USD 123.000000
Paige Wilunda is withdrawing  USD 1400.000000
Paige Wilunda is depositing  USD 123.000000
Paige Wilunda is withdrawing  USD 1400.000000
Paige Wilunda is depositing  USD 123.000000
Paige Wilunda is withdrawing  USD 1400.000000
Mike Wegner is withdrawing  USD 5600.000000
Mike Wegner is depositing  USD 2344.000000
New account BALANCE is 4038.000000

 

Using sync.RWMutex

Go has another type of mutex called sync.RWMutex, a reader/writer mutual exclusion lock, that allows multiple readers to hold the mutex lock and a single writer. sync.RWMutex comes with two methods namely sync.Rlock() and sync.RUnlock() which are used when reading data. When writing , locking and unlocking using sync.RWmutex should be done using lock() and unLock() respectively. sync.RWMutex,  is more efficient than mutex in scenarios where we have a high number of reads and less writes.

Example

package main
 
import (
   "fmt"
   "sync"
   "time"
)
 
var (
   counter int
   rwLock  sync.RWMutex
   mx      sync.RWMutex
)
 
func increment() {
   mx.Lock()
   counter++
   mx.Unlock()
}
 
func read(wg *sync.WaitGroup) {
   rwLock.RLock()
   defer rwLock.RUnlock()
 
   fmt.Println("Reading locking...")
   time.Sleep(time.Second)
   fmt.Println("Reading unlocking...")
   wg.Done()
}
func write(wg *sync.WaitGroup) {
   rwLock.Lock()
   defer rwLock.Unlock()
 
   fmt.Println("Write locking...")
   time.Sleep(time.Second)
   fmt.Println("Write unlocking...")
   wg.Done()
}
 
func readerWriter(wg *sync.WaitGroup) {
   wg.Add(5)
   go write(wg)
   go read(wg)
   go read(wg)
   go read(wg)
   go write(wg)
   time.Sleep(time.Second)
   fmt.Println("Done ...")
   wg.Done()
}
 
func main() {
   var wg sync.WaitGroup
   readerWriter(&wg)
   wg.Wait()
   fmt.Println("Main thread is done")
}

Explanation

In the above example we define two functions read() and write() . The read method calls the sync.Rlock() and sync.RUnLock(). This informs RWMutex to allow an unlimited number of reads without locking. In the write function, we call sync.lock() and sync.UnLock() to inform RWMutex that writes are limited to one write operation. This means that if we have several writes operations, the RWMutex will only allow one write operation and only release the RWMutex when there is no write operation using the RWMutex.

In the main function, we call the readerWriter() function that calls several write and read operations. One thing to note in the output is that more read operations are called compared to write operations.

Output

$ go run -race main.go
Write locking...
Write unlocking...
Reading locking...
Reading locking...
Reading locking...
Done ...
Reading unlocking...
Reading unlocking...
Reading unlocking...
Write locking...
Main thread is done

 

Using mutex with struct type

It is always considered a good practice to keep mutex close to the data they are intending to protect. Consider a struct that wants to control how its fields are accessed and modified, using mutex embedded in the struct will make it possible to control field usage in a struct. In the next example, we learn how to add a mutex to a struct data type.

Example

package main
 
import (
   "fmt"
   "sync"
)
 
type User struct {
   depositAmount float32
   sync.Mutex
}
 
var BALANCE float32 = 12000
 
func (u *User) deposit(wg *sync.WaitGroup) {
   u.Lock()
   BALANCE += u.depositAmount
   u.Unlock()
   wg.Done()
}
 
func main() {
 
   var wg sync.WaitGroup
   user := User{depositAmount: 1200}
   for i := 0; i < 10; i++ {
       wg.Add(3)
       go user.deposit(&wg)
       go user.deposit(&wg)
       go user.deposit(&wg)
   }
 
   wg.Wait()
 
   fmt.Printf("New account BALANCE is %f \n", BALANCE)
}

Explanation

In the preceding example, we create a struct data type called User, with two fields, one is depositAmount of type float32 and mutex type from the sync package. The User struct type has a method called deposit() that increments the BALANCE  variable. In the main function, we initialize a new user of type User, and loop through from 0 to 10 as we call the deposit() method.

Output

$ go run -race main.go
New account BALANCE is 48000.000000

 

Summary

In this tutorial we learn about race conditions , Mutex and RWMutex. Mutex helps goroutines to have controlled access and modification to common resources like data in memory. Mutex and RWMutex use a lock and unlock mechanism when giving access to data. Mutex is mostly used in scenarios where both read and write operations are almost the same . On the other hand, RWMutex is used in scenarios where read operations are more than write operations.

 

References

golang concurrency
golang mutex example

 

Related Keywords: golang mutex, mutex lock, mutex unlock, sync,RWMutex, sync.RLock, sync.RUnlock

 

Antony Shikubu

Antony Shikubu

He is highly skilled software developer with expertise in Python, Golang, and AWS cloud services. Skilled in building scalable solutions, he specializes in Django, Flask, Pandas, and NumPy for web apps and data processing, ensuring robust and maintainable code for diverse projects. You can reach out to him on his LinkedIn profile.

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can send mail to admin@golinuxcloud.com

Thank You for your support!!

Leave a Comment