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))
}