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.
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:
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:
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
:
sync.RWMutex.Lock
(write lock) can only be acquired if the write lock has not already been acquired ANDsync.RWMutex.RLock
(read lock) has not been acquired.sync.RWMutex.RLock
(read lock) can only be acquired ifsync.RWMutex.Lock
(write lock) has not been acquired.Both
sync.RWMutex.Lock
(write lock) andsync.RWMutex.RLock
(read lock) are blocking. As such,sync.RWMutex.Lock
(write lock) waits for all thesync.RWMutex.RLock
(read lock) or async.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 async.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?
- ```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.
package main
import (
"sync"
)
func main() {
m := &sync.RWMutex{}
m.Lock()
m.Unlock()
m.RLock()
m.Lock()
}
4.
package main
import (
"sync"
)
func main() {
m := &sync.RWMutex{}
m.Lock()
m.Unlock()
m.RLock()
m.RUnlock()
m.Lock()
}
5.
package main
import (
"sync"
)
func main() {
m := &sync.RWMutex{}
m.RLock()
m.RLock()
m.RLock()
m.RUnlock()
m.Lock()
}
6.
package main
import (
"sync"
)
func main() {
<span class="n">m</span> <span class="o">:=</span> <span class="o">&</span><span class="n">sync</span><span class="o">.</span><span class="n">RWMutex</span><span class="p">{}</span>
<span class="n">m</span><span class="o">.</span><span class="n">RLock</span><span class="p">()</span>
<span class="n">m</span><span class="o">.</span><span class="n">RLock</span><span class="p">()</span>
<span class="n">m</span><span class="o">.</span><span class="n">RLock</span><span class="p">()</span>
<span class="n">m</span><span class="o">.</span><span class="n">RUnlock</span><span class="p">()</span>
<span class="n">m</span><span class="o">.</span><span class="n">RUnlock</span><span class="p">()</span>
<span class="n">m</span><span class="o">.</span><span class="n">RUnlock</span><span class="p">()</span>
<span class="n">m</span><span class="o">.</span><span class="n">Lock</span><span class="p">()</span>
}
Top comments (0)