Go Locking Pitfall
Can you see (and explain) the issue with the following (slightly contrived) code?
package main
import (
"fmt"
"sync"
)
type Example struct {
m sync.RWMutex
a int
}
func (e *Example) ReadA() int {
defer e.m.RUnlock()
e.m.RLock()
return e.a
}
func (e *Example) WorkWithA() int {
defer e.m.RUnlock()
e.m.RLock()
val := e.ReadA()
val += 1
return val
}
func (e *Example) UpdateA(newA int) {
defer e.m.Unlock()
e.m.Lock()
e.a = newA
}
func main() {
ex := &Example{}
ex.UpdateA(1)
fmt.Println(ex.ReadA())
}
Nothing right? At least that’s what I thought. It seems to run without issues.
But if you add locks it probably means you want to run things concurrently, e.g. one go routine calling WorkWithA()
and another one calling UpdateA()
. And that should work fine, because things are properly locked.
But boiled down to the following code, it does not seem to run that well.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var m sync.RWMutex
// First RLock(), the one in WorkWithA()
m.RLock()
// An async routine that has to get a proper write lock first
go func() {
defer m.Unlock()
m.Lock()
// call e.UpdateA(..)
}()
// give routine time to start/run
time.Sleep(time.Second * 2)
// Get the second lock, the one in ReadA()
m.RLock()
// ReadA() returns its valie
m.RUnlock()
// WorkWithA() would do its thing with the returned value and RUnlock() when done
m.RUnlock()
fmt.Println("Done")
}
This simulates a possible locking order with an async task attempting to do a write (with associated Lock()
) while WorkWithA()
is active.
The order of locks is
RLock()
(first one inWorkWithA()
)- Lock() (through a concurrent
UpdateA()
) which would block/wait because there’s an active RLock() - Rlock() (in
Read()
) but that shouldn’t block because we already have an RLock(), and the Lock() has to wait - Release both
RLock()
s, whenReadA()
completes and then whenWorkWithA()
completes
You would assume the Lock()
will block until both RLock()
s have been released and it does, but it will also make the second RLock() block!
fatal error: all goroutines are asleep - deadlock!
The latter case was totally unexpected to me. I thought acquiring two RLocks() in succession would be fine, but if a Lock() is attempted in between the calls, it will make the second RLock() block because the Lock() will be the next in line to acquire the lock, once the first RLock() completes, which will never happen in this case (or any case)
Learnings
Doing two RLock()s in succession, e.g. by calling a function that RLock()s after the first RLock() can block if an async routine attempts A Lock() in between.