Golang has mutexes (short for mutually exclusion) to manage concurrent access to a shared object via multiple goroutines.

Here’s an example (taken from the excellent Go by Example: Mutexes).

package main

import (
	"fmt"
	"sync"
)

type Counter struct {
	mu sync.Mutex
	count map[string]int
}

func (c *Counter) Inc(name string) {

	c.mu.Lock()
	defer c.mu.Unlock()
	c.count[name]++
}

func main() {

	cnt := Counter{count: map[string]int{"james": 0, "spartacus": 0}}

	var wg sync.WaitGroup

	increment := func(name string, n int) {
		for i := 0 ; i < n ; i++ {
			cnt.Inc(name)
		}
		wg.Done()
	}

	wg.Add(3)
	go increment("james", 100000)
	go increment("james", 100000)
	go increment("spartacus", 10000)

	wg.Wait()
	fmt.Println(cnt)
}

This code defines a simple Counter struct with a single method named Inc to increment the count map. Inc is responsible for managing the locking and unlocking of the mutex with c.mu.Lock() to lock it and defer c.mu.Unlock() to unlock it as the method is returning.

With the above code, we get this output:

$ go run main.go
{{0 0} map[james:200000 spartacus:10000]}

But I wondered… What happens if we stop using the mutex? Do we get unexpected numbers?

Nope! We crash when we write to the map concurrently:

$ go run main.go
fatal error: concurrent map writes

Interesting.

That lead to me reading on the differences between a sync.Mutex and a sync.RWMutex. sync.RWMutex can have multiple readers and only a single writer, whereas sync.Mutex can have (I believe) only a single reader or writer at a time.

I decided to test the performance differences between sync.Mutex and sync.RWMutex with the below code:

package main

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

type Counter struct {
	mu sync.RWMutex  // <----- RWMutex
	count map[string]int
}

func (c *Counter) Read(name string) int {
	c.mu.RLock() // <--------- RLock()
	defer c.mu.RUnlock() // <- RUnlock()

	time.Sleep(1 * time.Millisecond)

	return c.count[name]
}

func main() {

	cnt := Counter{count: map[string]int{"james": 42}}

	var wg sync.WaitGroup

	readit := func(name string, n int) {
		for i := 0 ; i < n ; i++ {
			_ = cnt.Read("name")
		}
		wg.Done()
	}


	wg.Add(3)
	start := time.Now()
	go readit("james", 10000)
	go readit("james", 10000)
	go readit("spartacus", 10000)
	wg.Wait()

	end := time.Now()
	fmt.Println("duration:", end.Sub(start))
}

This took 11s to run:

$ go run main.go
duration: 11.529746917s

I then switched it to use a sync.Mutex (without concurrent reads) and suddenly the time shot up to 34s!!!

package main

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

type Counter struct {
	mu sync.Mutex
	count map[string]int
}

func (c *Counter) Read(name string) int {
	c.mu.Lock()
	defer c.mu.Unlock()

	time.Sleep(1 * time.Millisecond)

	return c.count[name]
}

func main() {

	cnt := Counter{count: map[string]int{"james": 42}}

	var wg sync.WaitGroup

	readit := func(name string, n int) {
		for i := 0 ; i < n ; i++ {
			_ = cnt.Read("name")
		}
		wg.Done()
	}


	wg.Add(3)
	start := time.Now()
	go readit("james", 10000)
	go readit("james", 10000)
	go readit("spartacus", 10000)
	wg.Wait()

	end := time.Now()
	fmt.Println("duration:", end.Sub(start))
}