Mutex and RWMutex in Go
Contents
Mutex and RWMutex in Go#
Go has builtin facilities for writing concurrent programs. The
concurrency pattern is implemented with CSP(Communication Sequential
Processes) model that was introduced by Tony Hoare in 1978. The
concurrent code in Go is written using goroutines and channels.
Goroutines are functions that run simultaneously and usually use
channels to synchronize with each other. To run a function as a
goroutine, it must be invocated with go
keyword: go listen()
Goroutines can have a common shared state and communication to access that state can be done via channels or via just accessing that shared state. The popular Go proverb is:
Don’t communicate by sharing memory, share memory by communicating.
That is, communication is done better and clearer when you share the state via channels through goroutines than directly accessing the shared state.
This blog post does cover the channel communication. It explains how to safely access the shared state using mutual exclusions in Go.
Mutexes are used to protect the shared state from mutation by multiple
goroutines at the same time. The protection is needed to avoid the
undefined behavior of the program. Go memory model does not guarantee
the correct work if there are data races. That is, one goroutine writes
to a shared variable neither before nor after another goroutine’s
write/read happened. They are doing it simultaneously. Fortunately, Go
runtime has a race detector, it is enabled with passing -race
flag
to the compiler:
go build -race
go test . -race
The sync
package implements two types of mutexes:
Mutex
RWmutex
Mutex#
The sync.Mutex
implements sync.Locker
interface and has two
methods:
Lock()
Unlock()
Lock()
acquires the lock and if another goroutine will call
Lock()
– it will be blocked until the Unlock()
will not release
the lock and makes it available for other goroutines. So, the lock must
be held while the shared state is being mutated. For example we a map
and two functions, one mutates it, another one reads from it:
package main
var m = map[string]int{}
func mutate(key string, val int) {
m[k] = v
return
}
func state(key string) (int, bool) {
val, ok := m[key]
return val, ok
}
func main() {
mutate("foo", 1)
v, ok := state("foo")
println(v, ok)
}
It is ok since reading/writing to the map is not happening at the same time. There is no concurrency in the code. Go memory model guarantees the order of execution of instructions that are written in the code: state starts after mutating returns.
But if we want to execute mutate
concurrently, with a go
keyword, race detector will warn about the possible data race. Multiple
goroutines must synchronize and change the shared variable atomically to
establish happens-before
conditions.
We have two goroutines that execute mutate and state functions concurrently. There can be a momentum when one goroutine reads state and another one changes it at the same time and this will be a data race that will bring to the memory corruption. To avoid this, goroutines must use synchronization primitives while accessing the shared stated. In other words, concurrent operations must be done atomically(consequently) but not at same time. There we start protecting memory with the mutex and our initial version of the code has changed:
package main
import (
"sync"
)
var m = map[string]int{}
var mutex = new(sync.Mutex)
func mutate(key string, val int) {
mutex.Lock()
m[key] = val
mutex.Unlock()
return
}
func state(key string) (int, bool) {
var val int
var ok bool
mutex.Lock()
val, ok = m[key]
mutex.Unlock()
return val, ok
}
func main() {
go mutate("foo", i)
val, ok := state("foo", i)
println(val, ok)
}
This makes concurrent read/write operations safely and there will not be data races. The map’s state is read and written atomically. If the goroutine #1 is reading the state it acquires the lock. Then, when goroutine #2 want to change/read the state at the same time, it has to wait until the lock will not be released by the goroutines #1. That’s ok for now and we are satisfied with that.
But, what if we change the state once in an hour and read every second.
Reading the state concurrently does mutate the shared state and it is
race free. The idea is to let multiple goroutines to hold the lock for
reading, but only one goroutine can hold the lock for writing. There
comes a RWMutex
!
RWMutex#
RWMutex
or read-write mutex allows multiple goroutines to hold the
read lock but only one goroutine can hold the write lock:
A RWMutex is a reader/writer mutual exclusion lock. The lock can be held by an arbitrary number of readers or a single writer. The zero value for an RWMutex is an unlocked mutex.
RWMutex
has added a couple more methods to acquire and release the
lock only for reading:
RLock()
acquires the lock for reading, and it can be held by multiple goroutines.RUnlock()
releases the single RLock().
Lock()
locks the state for writing, and if the lock is held by
goroutines for reading, it waits until the read lock is released and
does not let other goroutines to acquire the lock:
Lock locks rw for writing. If the lock is already locked for reading or writing, Lock blocks until the lock is available. If a goroutine holds a RWMutex for reading and another goroutine might call Lock, no goroutine should expect to be able to acquire a read lock until the initial read lock is released. In particular, this prohibits recursive read locking. This is to ensure that the lock eventually becomes available; a blocked Lock call excludes new readers from acquiring the lock.
The second version of the code that used Mutex will be changed:
package main
import (
"sync"
"time"
)
var m = map[string]int{}
var mutex = new(sync.RWMutex)
func mutate(key string, val int) {
mutex.Lock()
m[key] = val
mutex.Unlock()
return
}
func state(key string) (int, bool) {
mutex.RLock()
val, ok := m[key]
mutex.RUnlock()
return val, ok
}
func main() {
readTicker := time.NewTicker(100 * time.Millisecond)
go func() {
for _ = range readTicker.C {
state("foo")
}
}()
writeTicker := time.NewTicker(500 * time.Millisecond)
go func() {
for _ = range writeTicker.C {
mutate("foo", 1)
}
}()
time.Sleep(1600 * time.Millisecond)
writeTicker.Stop()
readTicker.Stop()
}