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.
- Race condition example
- Locking and Unlocking with Mutex
- Using
sync.RWMutex - 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 variablemof typeMutex. This variable will be used to access theLock()andUnlock()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

![Golang Mutex Tutorial [Lock() & Unlock() Examples]](/golang-mutex/golang_mutex.jpg)
