项目作者: xaionaro-go

项目描述 :
goroutine mutual exclusion (aka recursive mutex for Go)
高级语言: Go
项目地址: git://github.com/xaionaro-go/gorex.git
创建时间: 2020-03-08T15:53:12Z
项目社区:https://github.com/xaionaro-go/gorex

开源协议:Creative Commons Zero v1.0 Universal

下载


GoDoc
go report
Build Status
Coverage Status



CC0

gorex

gorex == GORoutine mutual EXclusion

This package implements Mutex and RWMutex. They are similar to sync.Mutex and sync.RWMutex, but
they track which goroutine locked the mutex and will not cause a deadlock if
the same goroutine will try to lock the same mutex again.

  1. type myEntity struct {
  2. gorex.Mutex
  3. }
  4. func (ent *myEntity) func1() {
  5. ent.Lock()
  6. defer ent.Unlock()
  7. .. some stuff ..
  8. ent.func2() // will not get a deadlock here!
  9. .. other stuff ..
  10. }
  11. func (ent *myEntity) func2() {
  12. ent.Lock()
  13. defer ent.Unlock()
  14. .. more stuff ..
  15. }

The same in other syntax:

  1. type myEntity struct {
  2. gorex.Mutex
  3. }
  4. func (ent *myEntity) func1() {
  5. ent.LockDo(func() {
  6. .. some stuff ..
  7. ent.func2() // will not get a deadlock here!
  8. .. other stuff ..
  9. })
  10. }
  11. func (ent *myEntity) func2() {
  12. ent.LockDo(func(){
  13. .. more stuff ..
  14. })
  15. }
  1. locker := &goroutine.RWMutex{}
  2. locker.RLockDo(func() {
  3. .. do some read-only stuff ..
  4. if cond {
  5. return
  6. }
  7. locker.LockDo(func() { // will not get a deadlock here!
  8. .. do write stuff ..
  9. })
  10. })

But…

But you still will get a deadlock if you do this way:

  1. var locker = &gorex.RWMutex{}
  2. func someFunc() {
  3. locker.RLockDo(func() {
  4. .. do some read-only stuff ..
  5. if cond {
  6. return
  7. }
  8. locker.LockDo(func() { // you will get a deadlock here!
  9. .. do write stuff ..
  10. })
  11. })
  12. }()
  13. func main() {
  14. go someFunc()
  15. go someFunc()
  16. }

because there could be a situation that a resource is blocked by a RLockDo from
both goroutines and both goroutines waits (on LockDo) until other goroutine
will finish RLockDo. But still you will easily see the reason of deadlocks due
to LockDo-s in the call stack trace.

Benchmark

It’s essentially slower than bare sync.Mutex/sync.RWMutex:

  1. goos: linux
  2. goarch: amd64
  3. pkg: github.com/xaionaro-go/gorex
  4. Benchmark/Lock-Unlock/single/sync.Mutex-8 77933413 15.2 ns/op 0 B/op 0 allocs/op
  5. Benchmark/Lock-Unlock/single/sync.RWMutex-8 46052574 26.1 ns/op 0 B/op 0 allocs/op
  6. Benchmark/Lock-Unlock/single/Mutex-8 20281420 58.6 ns/op 0 B/op 0 allocs/op
  7. Benchmark/Lock-Unlock/single/RWMutex-8 13518639 87.1 ns/op 0 B/op 0 allocs/op
  8. Benchmark/Lock-Unlock/parallel/sync.Mutex-8 10836991 111 ns/op 0 B/op 0 allocs/op
  9. Benchmark/Lock-Unlock/parallel/sync.RWMutex-8 9065725 133 ns/op 0 B/op 0 allocs/op
  10. Benchmark/Lock-Unlock/parallel/Mutex-8 9425310 123 ns/op 2 B/op 0 allocs/op
  11. Benchmark/Lock-Unlock/parallel/RWMutex-8 5309696 213 ns/op 4 B/op 0 allocs/op
  12. Benchmark/RLock-RUnlock/single/sync.RWMutex-8 76609815 15.2 ns/op 0 B/op 0 allocs/op
  13. Benchmark/RLock-RUnlock/single/RWMutex-8 25071478 47.9 ns/op 0 B/op 0 allocs/op
  14. Benchmark/RLock-RUnlock/parallel/sync.RWMutex-8 25705654 48.3 ns/op 0 B/op 0 allocs/op
  15. Benchmark/RLock-RUnlock/parallel/RWMutex-8 14786738 80.9 ns/op 0 B/op 0 allocs/op
  16. Benchmark/Lock-ed:Lock-Unlock/single/Mutex-8 31392260 38.2 ns/op 0 B/op 0 allocs/op
  17. Benchmark/Lock-ed:Lock-Unlock/single/RWMutex-8 32588916 37.6 ns/op 0 B/op 0 allocs/op
  18. Benchmark/RLock-ed:RLock-RUnlock/single/RWMutex-8 26416754 46.1 ns/op 0 B/op 0 allocs/op
  19. Benchmark/RLock-ed:RLock-RUnlock/parallel/RWMutex-8 13113901 88.7 ns/op 0 B/op 0 allocs/op
  20. PASS
  21. ok github.com/xaionaro-go/gorex 20.321s

But sometimes it allows you to think more about strategic problems
(“this stuff should be edited atomically, so I’ll be able to…”)
instead of wasting time on tactical problems (“how to handle those locks”) :)

If you still have a Deadlock…

Of course this package does not solve all possible reasons of deadlocks,
but it also provides a way to debug what’s going on. First of all,
I recommend you to use LockDo instead of bare Lock when possible:

  • It will make sure you haven’t forgot to unlock the mutex.
  • It will be shown in the call stack trace (so you’ll see what’s going on).

If you already have a problem with a deadlock, then I recommend you to write
and unit/integration test which can reproduce the deadlock situation and then
limit by time gorex.DefaultInfiniteContext.
On a deadlock it will panic and will show the call stack trace of every routine
which holds the lock.

For example in my case I saw:

  1. monopolized by:
  2. /home/xaionaro/.gimme/versions/go1.13.linux.amd64/src/runtime/proc.go:2664 (runtime.goexit1)
  3. panic: The InfiniteContext is done...

So it seems a routine already exited (and never released the lock). So a support
of the build tag deadlockdebug was added, which will print a call
stack trace of a lock which was never released (and goroutine already exited). Specifically
in my case it printed:

  1. $ go test ./... -timeout 1s -bench=. -benchtime=100ms -tags deadlockdebug
  2. ...
  3. an opened lock {LockerPtr:824636154976 IsWrite:true} which was never released (and the goroutine already exited):
  4. /home/xaionaro/go/pkg/mod/github.com/xaionaro-go/gorex@v0.0.0-20200308222358-b650fa4b5b14/rw_mutex.go:72 (github.com/xaionaro-go/gorex.(*RWMutex).lock)
  5. /home/xaionaro/go/pkg/mod/github.com/xaionaro-go/gorex@v0.0.0-20200308222358-b650fa4b5b14/rw_mutex.go:43 (github.com/xaionaro-go/gorex.(*RWMutex).Lock)
  6. /home/xaionaro/go/src/github.com/xaionaro-go/secureio/session.go:1480 (github.com/xaionaro-go/secureio.(*Session).sendDelayedNow)
  7. /home/xaionaro/go/src/github.com/xaionaro-go/secureio/session.go:1774 (github.com/xaionaro-go/secureio.(*Session).startKeyExchange.func2)
  8. /home/xaionaro/go/src/github.com/xaionaro-go/secureio/key_exchanger.go:449 (github.com/xaionaro-go/secureio.(*keyExchanger).sendSuccessNotifications)
  9. /home/xaionaro/go/src/github.com/xaionaro-go/secureio/key_exchanger.go:408 (github.com/xaionaro-go/secureio.(*keyExchanger).Handle.func2)
  10. /home/xaionaro/go/src/github.com/xaionaro-go/secureio/key_exchanger.go:242 (github.com/xaionaro-go/secureio.(*keyExchanger).LockDo)
  11. ...

So I opened line session.go:1480 added defer sess.delayedWriteBuf.Unlock() and it fixed the problem :)

Comparison with other implementations

I found 2 other implementations:

The first one is broken:

  1. panic: unsupported go version go1.13

The second one sleeps in terms of millisecond, which:

  • Give good result if the lock is short-living: performance is much better (than here).
  • Continuously consumes CPU resources on long-living locks, while I’m developing an application for mobile phones and would like to avoid such problems.
  • Does not support RLock/RUnlock.