The Difference between sync.Mutex and sync.RWMutex in Golang Explained in Simple Terms
Tags: go
Published: 2023-11-20 08:11:42


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nak7p3428ye3d7vx01b2.png)



## What is Mutex?

**Mutex (mutual exclusion)** is an object used to **synchronise concurrent threads** accessing a shared resource to **prevent race conditions**, which is a situation that happens when multiple concurrent threads **access a shared resource and try to modify it**, leading to an incorrect data or an unexpected behaviour.

## Race Condition Example in Go

The below program demonstrates a race condition where we start with a shared resource variable of type integer initialised to zero, and we increment it in one loop and decrement it in another, both happening at the same time concurrently using goroutines. We use `sync.WaitGroup` to wait for all the goroutines to finish before terminating the program. At the end of the execution, we expect resource to be back to 0, but it does not. This is known as race condition as multiple goroutines tries to access and modify the resource at the same time which leads to incorrect data.

```golang
package main

import (
	"fmt"
	"sync"
)

var resource int32 = 0

func Increment(wg *sync.WaitGroup) {
	resource = resource + 1
	wg.Done()
}

func Decrement(wg *sync.WaitGroup) {
	resource = resource - 1
	wg.Done()
}

func main() {
	const ITERATIONS = 500
	wg := &sync.WaitGroup{}

	for i := 0; i < ITERATIONS; i++ {
		wg.Add(1)
		go Increment(wg)
	}

	for i := 0; i < ITERATIONS; i++ {
		wg.Add(1)
		go Decrement(wg)
	}

	wg.Wait()

	fmt.Println(resource) // does not print 0
}
```

## Using Golang's `sync.Mutex`

To fix the race condition in the code above, we can use a mutex. A mutex is like a lock that we could acquire before allowing the program control to the critical section of the code to the shared resource. This allows only one goroutine to access the resource at a time, ensuring no conflicts occur. We lock it when one goroutine is using it, and unlock it when it's done, so the next one can use it. This ensures that the operations on resource happen one at a time, preventing conflicts and giving the expected result. Golang provides mutex in the `sync` package. As such, we can use `sync.Mutex` to implement a mutex as follows:

```golang
package main

import (
	"fmt"
	"sync"
)

var resource int32 = 0

func Increment(wg *sync.WaitGroup, mu *sync.Mutex) {
	mu.Lock()
	defer mu.Unlock()

	resource = resource + 1
	wg.Done()
}

func Decrement(wg *sync.WaitGroup, mu *sync.Mutex) {
	mu.Lock()
	defer mu.Unlock()

	resource = resource - 1
	wg.Done()
}

func main() {
	const ITERATIONS = 500
	wg := &sync.WaitGroup{}
	mu := &sync.Mutex{}

	for i := 0; i < ITERATIONS; i++ {
		wg.Add(1)
		go Increment(wg, mu)
	}

	for i := 0; i < ITERATIONS; i++ {
		wg.Add(1)
		go Decrement(wg, mu)
	}

	wg.Wait()

	fmt.Println(resource) // prints 0
}
```

By implementing a mutex as shown above, we are able to prevent the race condition that we saw earlier and the value of the `resource` variable does indeed become zero at the end of the execution.

##The Shortcomings of `sync.Mutex`

`sync.Mutex` provides an **exclusive lock**, also known as **write lock** where only one thread can access the shared resource at a time before we modify the resource. However, what if we would like to have **read lock** where we can have multiple threads reading a shared resource and while it is being read, we prevent any other threads from modifying the resource? This is where `sync.RWMutex` comes into picture.

`sync.RWMutex` is similar to `sync.Mutex` where it provides **write lock**, i.e., `sync.RWMutex.Lock`. In addition to that, `sync.RWMutex` also provides **read lock**, i.e., `sync.RWMutex.RLock`. Let's see these locks in action below:

```golang
package main

import (
	"fmt"
	"sync"
	"time"
)

var resource int32 = 0

func Read(rwm *sync.RWMutex, wg *sync.WaitGroup) {

	rwm.RLock()
	fmt.Println("read lock acquired")

	time.Sleep(time.Second * 3)

	fmt.Println("read lock released")
	rwm.RUnlock()
	wg.Done()
}

func Write(rwm *sync.RWMutex, wg *sync.WaitGroup) {

	rwm.Lock()
	fmt.Println("write lock acquired")

	resource = resource + 1
	time.Sleep(time.Second * 3)

	fmt.Println("write lock released")
	rwm.Unlock()
	wg.Done()

}

func main() {
	rwm := &sync.RWMutex{}
	wg := &sync.WaitGroup{}

	for i := 0; i < 5; i++ {
		wg.Add(2)
		go Write(rwm, wg)
		go Read(rwm, wg)
	}

	wg.Wait()
}
```

**Sample Output:**
```
read lock acquired
read lock acquired
read lock released
read lock released
write lock acquired
write lock released
read lock acquired
read lock acquired
read lock acquired
read lock released
read lock released
read lock released
write lock acquired
write lock released
write lock acquired
write lock released
write lock acquired
write lock released
write lock acquired
write lock released
```

In the output above, we can observe that we were able to acquire multiple read locks (`sync.RWMutex.RLock`) at the same time, in contrary to the write locks (`sync.RWMutex.Lock`) which we were only able to acquire one lock at a time. As such, the following are true for `sync.RWMutex`:

1. `sync.RWMutex.Lock`(write lock) can only be acquired if the write lock has not already been acquired AND `sync.RWMutex.RLock` (read lock) has not been acquired.

2. `sync.RWMutex.RLock` (read lock) can only be acquired if `sync.RWMutex.Lock` (write lock) has not been acquired.

3. Both `sync.RWMutex.Lock` (write lock) and `sync.RWMutex.RLock` (read lock) are blocking. As such, `sync.RWMutex.Lock` (write lock) waits for all the `sync.RWMutex.RLock` (read lock) or a `sync.RWMutex.Lock` (write lock) that were acquired earlier to be released before being able to acquire a lock. On the other hand, `sync.RWMutex.RLock` (read lock) waits for a  `sync.RWMutex.Lock` (write lock) that was acquired earlier to be released before being able to acquire a lock. 

## Conclusion
In conclusion, both `sync.Mutex` and `sync.RWMutex` are Golang primitives that provide mutex. `sync.Mutex` only provides a read lock mutex. On the other hand, `sync.RWMutex` provides both read lock and write lock mutex. The decision as to which one to use depends on the use case. If the application mostly does reads and occasional writes, then `sync.RWMutex` would be suitable. Contrarily, if the application mostly does writes and occasional reads, then `sync.Mutex` would be suitable.  

## Test your Understanding

The following programs demonstrate how the order of acquiring read or write locks results in successful lock acquirement. Some of these programs may panic due to deadlock. Which of the following programs may or may not panic?

1. 
```golang
package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	
	m := &sync.Mutex{}

	m.Lock()

	m.Lock()
}
```

2.
```golang
package main

import (
	"sync"
)

func main() {

	m := &sync.Mutex{}

	m.Lock()
	m.Unlock()

	m.Lock()
}
```

3.
```golang
package main

import (
	"sync"
)

func main() {

	m := &sync.RWMutex{}

	m.Lock()
	m.Unlock()

	m.RLock()

	m.Lock()
}
```

4.
```golang
package main

import (
	"sync"
)

func main() {

	m := &sync.RWMutex{}

	m.Lock()
	m.Unlock()

	m.RLock()
	m.RUnlock()

	m.Lock()
}
```

5.
```golang
package main

import (
	"sync"
)

func main() {

	m := &sync.RWMutex{}

	m.RLock()
	m.RLock()
	m.RLock()

	m.RUnlock()

	m.Lock()
}
```

6.
```golang
package main

import (
	"sync"
)

func main() {

	m := &sync.RWMutex{}

	m.RLock()
	m.RLock()
	m.RLock()

	m.RUnlock()
	m.RUnlock()
	m.RUnlock()

	m.Lock()
}
```

## Reference
1. https://programmer.ink/think/go-mutex-sync.mutex-and-read-write-lock-sync.rwmutex.html