Recursive (aka reentrant) mutexes are a bad idea.
The fundamental reason to use a mutex is that mutexes
protect invariants, perhaps internal invariants like
"p.Prev.Next == p for all elements of the ring,'' or perhaps
external invariants like "my local variable x is equal to p.Prev.''
Locking a mutex asserts "I need the invariants to hold''
and perhaps "I will temporarily break those invariants.''
Releasing the mutex asserts "I no longer depend on those
invariants'' and "If I broke them, I have restored them.''
Understanding that mutexes protect invariants is essential to
identifying where mutexes are needed and where they are not.
For example, does a shared counter updated with atomic
increment and decrement instructions need a mutex?
It depends on the invariants. If the only invariant is that
the counter has value i - d after i increments and d decrements,
then the atmocity of the instructions ensures the
invariants; no mutex is needed. But if the counter must be
in sync with some other data structure (perhaps it counts
the number of elements on a list), then the atomicity of
the individual operations is not enough. Something else,
often a mutex, must protect the higher-level invariant.
This is the reason that operations on maps in Go are not
guaranteed to be atomic: it would add expense without
benefit in typical cases.
Let's take a look at recursive mutexes.
Suppose we have code like this:
```go
func F() {
mu.Lock()
... do some stuff ...
G()
... do some more stuff ...
mu.Unlock()
}
func G() {
mu.Lock()
... do some stuff ...
mu.Unlock()
}
```
Normally, when a call to mu.Lock returns, the calling code
can now assume that the protected invariants hold, until
it calls mu.Unlock.
A recursive mutex implementation would make G's mu.Lock
and mu.Unlock calls be no-ops when called from within F
or any other context where the current thread already holds mu.
If mu used such an implementation, then when mu.Lock
returns inside G, the invariants may or may not hold. It depends
on what F has done before calling G. Maybe F didn't even realize
that G needed those invariants and has broken them (entirely
possible, especially in complex code).
Recursive mutexes do not protect invariants.
Mutexes have only one job, and recursive mutexes don't do it.
There are simpler problems with them, like if you wrote
```go
func F() {
mu.Lock()
... do some stuff
}
```
you'd never find the bug in single-threaded testing.
But that's just a special case of the bigger problem,
which is that they provide no guarantees at all about
the invariants that the mutex is meant to protect.
If you need to implement functionality that can be called
with or without holding a mutex, the clearest thing to do
is to write two versions. For example, instead of the above G,
you could write:
// To be called with mu already held.
// Caller must be careful to ensure that ...
```go
func g() {
... do some stuff ...
}
func G() {
mu.Lock()
g()
mu.Unlock()
}
```
or if they're both unexported, g and gLocked.
I am sure that we'll need TryLock eventually; feel free to
send us a CL for that. Lock with timeout seems less essential
but if there were a clean implementation (I don't know of one)
then maybe it would be okay. Please don't send a CL that
implements recursive mutexes.
Recursive mutexes are just a mistake, nothing more than
a comfortable home for bugs.
link:<a href:"https://groups.google.com/g/golang-nuts/c/XqW1qcuZgKg/m/Ui3nQkeLV80J?pli=1">https://groups.google.com/g/golang-nuts/c/XqW1qcuZgKg/m/Ui3nQkeLV80J?pli=1<a/>
补一下Go程序设计语言上面关于这个问题的解答:
Go语言的互斥量是不可再入的,具体理由见后.互斥量的目的是在程序执行过程中维持基于共享变量的特定不变量.其中一个不变量是"没有goroutine正在访问这个共享变量",但有可能互斥量也保护针对数据结构的其他不变量.当goroutine获取一个互斥锁的时候,它可能会假定这些不变量是满足的.当它获取到互斥锁之后,它可能保证之前的不变量已经还原且又能重新满足.尽管一个可冲入的互斥量可以确保没有其他goroutine可以访问共享变量,但是这无法保护这些变量的其他不变量.(2017年第1版p207)
有疑问加站长微信联系(非本文作者))