Deadlock may easily occur if one method of cache
calls another method, and both contain the Lock()
call.
See this example:
func (this *cache) f1() {
this.mutex.Lock()
defer this.mutex.Unlock()
this.f2()
}
func (this *cache) f2() {
this.mutex.Lock()
defer this.mutex.Unlock()
}
func main() {
c := &cache{}
c.Init()
c.f1()
fmt.Println("Hello, playground")
}
Output (try it on the Go Playground):
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]:
sync.runtime_SemacquireMutex(0x1040a12c, 0x8)
/usr/local/go/src/runtime/sema.go:62 +0x40
sync.(*Mutex).Lock(0x1040a128, 0x10429f5c)
/usr/local/go/src/sync/mutex.go:87 +0xa0
main.(*cache).f2(0x10429f94, 0x1100c0)
/tmp/sandbox647646735/main.go:23 +0x40
main.(*cache).f1(0x10429f94, 0xdf6e0)
/tmp/sandbox647646735/main.go:19 +0xa0
main.main()
/tmp/sandbox647646735/main.go:30 +0x60
Note that there does not need to have a direct call from one method to the other, it may also be a transitive call. For example cache.f1()
may call foo()
which may be a "standalone" function, and if foo()
calls cache.f2()
, we're at the same deadlock.
Improvements:
Don't name your receiver this
, it is not idiomatic. You may simply call it c
. Read more about it here: In Go is naming the receiver variable 'self' misleading or good practice?
You may embed mutexes, making it convenient to use and eliminate the need for initialization. Read more about it here: When do you embed mutex in struct in Go?
type cache struct {
sync.Mutex
}
func (c *cache) f1() {
c.Lock()
defer c.Unlock()
c.f2()
}
func (c *cache) f2() {
c.Lock()
defer c.Unlock()
}
func main() {
c := &cache{}
c.f1()
fmt.Println("Hello, playground")
}
Of course this also causes a deadlock. Try it on the Go Playground. Also note that this inherently exposes the mutex (as the embedded type starts with lowecae letter), so anyone will be able to call the Lock()
and Unlock()
methods. Depends on case whether this is a problem.