- read

Go Concurrency Visually Explained – sync.Mutex

Brian NQC 111

Silver padlock photo — Free Lock Image on Unsplash

While learning Go programming language, you would likely encounter the famous adage: “Do not communicate by sharing memory; instead, share memory by communicating.” This mantra underpins Go’s powerful concurrency model, where channels serve as the primary conduit for communication between goroutines. However, while channels are a versatile tool for managing concurrency, it’s a misconception to assume that we should always replace traditional locking mechanisms like Mutex with them. There are situations where using a Mutex is not only appropriate but also more efficient than channels.

Countinuing my Go Concurrency Visually Explained blog series, today, I will be visually explaining sync.Mutex.

Golang Essentials

5 stories

Scenario

Imagine four Gopher cyclists and colleagues cycling to their office daily. They all need to take a shower after reaching the office, but there’s only one bathroom. To prevent chaos, they ensure that only one person can use the bathroom at a time. This concept of exclusive access is at the heart of Go’s Mutex (mutual exclusion).

Taking shower in the office each morning is a little competition for cyclists and runners

Normal Mode

Stringer is the earliest arriver today. No one is using the bathroom when he comes. Thus, he can immediately acquire the bathroom.

Lock() an unlocked Mutex succeeds immediately

A moment later, Partier arrives. Partier sees that there is someone using the bathroom but he has no idea who or when s/he will finish using it. At this point, he has two options: standing right in front of the bathroom (active waiting), or going somewhere and coming back later (passive waiting). In the Go lingo, the former is called “spinning”. Spinning goroutines hold CPU resource, increasing their of acquiring the Mutex quickly when it becomes available shortly, all without the overhead of context switching (known for being expensive). However, when the Mutex doesn’t likely to become available anytime soon, continuing to hold CPU resources will dismish the chances of other goroutines getting their share of CPU time.

As of version 1.21, Golang allows arriving goroutine to spin for awhile. However, if it cannot acquire the Mutex within a specified time frame, it will sleep, and give other goroutines a fair chance to run.

Arriving goroutines will first spin, then sleep

Candier comes. Just like Partier, she attempts to acquire the bathroom.

Because she recently comes, she has a big chance of acquiring the bathroom if Stringer releases it shortly, before she goes passive like Partier. This is called normal mode.

Normal mode has considerably better performance as a goroutine can acquire a mutex several times in a row even if there are blocked waiters.

go/src/sync/mutex.go at go1.21.0 · golang/go · GitHub

New arriving goroutines have an advantage in competing over the ownership

Starvation Mode

Partier is back. As he has been waiting for a long time (more than 1ms), he will try to acquire the bathroom in starvation mode. When Swimmer comes, he notices that someone is starved, he will not try to acquire the bathroom nor spin. Instead, he queues himself at the tail of the wait queue.

In this starvation mode, when Candier finishes, she directly hands off the bathroom to Partier. There is no competition at this point.

Starvation mode is important to prevent pathological cases of tail latency.

go/src/sync/mutex.go at go1.21.0 · golang/go · GitHub

Partier finishes his turn and releases the bathroom. At this time, only Swimmer is waiting therefore he will own it right away. Swimmer notices that he is the last one who waits, he sets the Mutex back to normal mode. Swimmer will also do the same if he sees his waiting time is less than 1ms.

Finally, Swimmer releases the bathroom after use. Please note that Mutex does not change the owners from LOCKED to LOCKEDstate. It will also transition from LOCKED to UNLOCKED then LOCKED state. Due to space constraints, I omit the middle state on the images above.

Show me your code!

The implementation of Mutex changes from time to time and in fact, it’s not easy to understand. Fortunately, we don’t have to fully understand its implementation in order to use it efficiently. If there is one thing you should remember from this blog, it must be: early arrivers don’t way win the competition. Instead, new arriving goroutines usually have higher chance as they are still on CPU. Golang also tries to avoid leaving waiters starved by implementing the starvation mode. If you want to go even deeper, the full source code of Mutex is available on GitHub.

package main

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

func main() {
wg := sync.WaitGroup{}
wg.Add(4)

bathroom := sync.Mutex{}

takeAShower := func(name string) {
defer wg.Done()

fmt.Printf("%s: I want to take a shower. I'm trying to acquire the bathroom\n", name)
bathroom.Lock()
fmt.Printf("%s: I have the bathroom now, taking a shower\n", name)
time.Sleep(500 * time.Microsecond)
fmt.Printf("%s: I'm done, I'm unlocking the bathroom\n", name)
bathroom.Unlock()
}

go takeAShower("Partier")
go takeAShower("Candier")
go takeAShower("Stringer")
go takeAShower("Swimmer")

wg.Wait()
fmt.Println("main: Everyone is Done. Shutting down...")
}

As you might guess, the results of concurrent code are almost always non deterministic.

# First time
Swimmer: I want to take a shower. I'm trying to acquire the bathroom
Partier: I want to take a shower. I'm trying to acquire the bathroom
Candier: I want to take a shower. I'm trying to acquire the bathroom
Stringer: I want to take a shower. I'm trying to acquire the bathroom
Swimmer: I have the bathroom now, taking a shower
Swimmer: I'm done, I'm unlocking the bathroom
Partier: I have the bathroom now, taking a shower
Partier: I'm done, I'm unlocking the bathroom
Candier: I have the bathroom now, taking a shower
Candier: I'm done, I'm unlocking the bathroom
Stringer: I have the bathroom now, taking a shower
Stringer: I'm done, I'm unlocking the bathroom
main: Everyone is Done. Shutting down...

# Second time
Swimmer: I want to take a shower. I'm trying to acquire the bathroom
Swimmer: I have the bathroom now, taking a shower
Partier: I want to take a shower. I'm trying to acquire the bathroom
Stringer: I want to take a shower. I'm trying to acquire the bathroom
Candier: I want to take a shower. I'm trying to acquire the bathroom
Swimmer: I'm done, I'm unlocking the bathroom
Partier: I have the bathroom now, taking a shower
Partier: I'm done, I'm unlocking the bathroom
Stringer: I have the bathroom now, taking a shower
Stringer: I'm done, I'm unlocking the bathroom
Candier: I have the bathroom now, taking a shower
Candier: I'm done, I'm unlocking the bathroom
main: Everyone is Done. Shutting down...

If you find this article helpful, please motivate me with one clap. You can also check out my other articles at https://medium.com/@briannqc and connect to me on LinkedIn. Thanks a lot for reading!

Thank you for reading until the end. Please consider following the writer and this publication. Visit Stackademic to find out more about how we are democratizing free programming education around the world.