gorex

package module
v0.0.0-...-f03b85f Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2024 License: CC0-1.0 Imports: 9 Imported by: 2

README

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.

type myEntity struct {
    gorex.Mutex
}

func (ent *myEntity) func1() {
    ent.Lock()
    defer ent.Unlock()

    .. some stuff ..
    ent.func2() // will not get a deadlock here!
    .. other stuff ..
}

func (ent *myEntity) func2() {
    ent.Lock()
    defer ent.Unlock()

    .. more stuff ..
}

The same in other syntax:

type myEntity struct {
    gorex.Mutex
}

func (ent *myEntity) func1() {
    ent.LockDo(func() {
        .. some stuff ..
        ent.func2() // will not get a deadlock here!
        .. other stuff ..
    })
}

func (ent *myEntity) func2() {
    ent.LockDo(func(){
        .. more stuff ..
    })
}
locker := &goroutine.RWMutex{}

locker.RLockDo(func() {
    .. do some read-only stuff ..
    if cond {
      return
    }
    locker.LockDo(func() { // will not get a deadlock here!
        .. do write stuff ..
    })
})
But...

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

var locker = &gorex.RWMutex{}

func someFunc() {
    locker.RLockDo(func() {
        .. do some read-only stuff ..
        if cond {
          return
        }
        locker.LockDo(func() { // you will get a deadlock here!
            .. do write stuff ..
        })
    })
}()

func main() {
    go someFunc()
    go someFunc()
}

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:

goos: linux
goarch: amd64
pkg: github.com/xaionaro-go/gorex
Benchmark/Lock-Unlock/single/sync.Mutex-8         	77933413	        15.2 ns/op	       0 B/op	       0 allocs/op
Benchmark/Lock-Unlock/single/sync.RWMutex-8       	46052574	        26.1 ns/op	       0 B/op	       0 allocs/op
Benchmark/Lock-Unlock/single/Mutex-8              	20281420	        58.6 ns/op	       0 B/op	       0 allocs/op
Benchmark/Lock-Unlock/single/RWMutex-8            	13518639	        87.1 ns/op	       0 B/op	       0 allocs/op
Benchmark/Lock-Unlock/parallel/sync.Mutex-8       	10836991	       111 ns/op	       0 B/op	       0 allocs/op
Benchmark/Lock-Unlock/parallel/sync.RWMutex-8     	 9065725	       133 ns/op	       0 B/op	       0 allocs/op
Benchmark/Lock-Unlock/parallel/Mutex-8            	 9425310	       123 ns/op	       2 B/op	       0 allocs/op
Benchmark/Lock-Unlock/parallel/RWMutex-8          	 5309696	       213 ns/op	       4 B/op	       0 allocs/op
Benchmark/RLock-RUnlock/single/sync.RWMutex-8     	76609815	        15.2 ns/op	       0 B/op	       0 allocs/op
Benchmark/RLock-RUnlock/single/RWMutex-8          	25071478	        47.9 ns/op	       0 B/op	       0 allocs/op
Benchmark/RLock-RUnlock/parallel/sync.RWMutex-8   	25705654	        48.3 ns/op	       0 B/op	       0 allocs/op
Benchmark/RLock-RUnlock/parallel/RWMutex-8        	14786738	        80.9 ns/op	       0 B/op	       0 allocs/op
Benchmark/Lock-ed:Lock-Unlock/single/Mutex-8      	31392260	        38.2 ns/op	       0 B/op	       0 allocs/op
Benchmark/Lock-ed:Lock-Unlock/single/RWMutex-8    	32588916	        37.6 ns/op	       0 B/op	       0 allocs/op
Benchmark/RLock-ed:RLock-RUnlock/single/RWMutex-8 	26416754	        46.1 ns/op	       0 B/op	       0 allocs/op
Benchmark/RLock-ed:RLock-RUnlock/parallel/RWMutex-8         	13113901	        88.7 ns/op	       0 B/op	       0 allocs/op
PASS
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:

monopolized by:
/home/xaionaro/.gimme/versions/go1.13.linux.amd64/src/runtime/proc.go:2664 (runtime.goexit1)
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:

$ go test ./... -timeout 1s -bench=. -benchtime=100ms -tags deadlockdebug
...
an opened lock {LockerPtr:824636154976 IsWrite:true} which was never released (and the goroutine already exited):
/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)
/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)
/home/xaionaro/go/src/github.com/xaionaro-go/secureio/session.go:1480 (github.com/xaionaro-go/secureio.(*Session).sendDelayedNow)
/home/xaionaro/go/src/github.com/xaionaro-go/secureio/session.go:1774 (github.com/xaionaro-go/secureio.(*Session).startKeyExchange.func2)
/home/xaionaro/go/src/github.com/xaionaro-go/secureio/key_exchanger.go:449 (github.com/xaionaro-go/secureio.(*keyExchanger).sendSuccessNotifications)
/home/xaionaro/go/src/github.com/xaionaro-go/secureio/key_exchanger.go:408 (github.com/xaionaro-go/secureio.(*keyExchanger).Handle.func2)
/home/xaionaro/go/src/github.com/xaionaro-go/secureio/key_exchanger.go:242 (github.com/xaionaro-go/secureio.(*keyExchanger).LockDo)
...

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:

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.

Documentation

Index

Constants

View Source
const GO_VERSION = ">=go1.23"

Variables

View Source
var DefaultInfiniteContext = context.Background()

DefaultInfiniteContext is used as the default context used on any try to lock if a custom context is not set (see LockCtx/RLockCtx), but with the difference if this context will be done, then it will panic with debugging information.

To specify a context with deadline may be useful for unit tests.

Functions

This section is empty.

Types

type G

type G struct{}

G is just a placeholder. It was supposed to be an alias to runtime.g.

func GetG

func GetG() *G

GetG is an alias to runtime.getg

type Mutex

type Mutex struct {
	// InfiniteContext is used as the default context used on any try to lock if
	// a custom context is not set (see LockCtx), but with the difference
	// if this context will be done, then it will panic with debugging information.
	//
	// To specify a context with deadline may be useful for unit tests.
	//
	// The zero-value means to use DefaultInfiniteContext.
	InfiniteContext context.Context
	// contains filtered or unexported fields
}

Mutex is a goroutine-aware analog of sync.Mutex, so it works the same way as sync.Mutex, but tracks which goroutine locked it. So it could be locked multiple times with the same routine.

func (*Mutex) Lock

func (m *Mutex) Lock()

Lock is analog of `(*sync.Mutex)`.Lock, but it allows one goroutine to call it multiple times without calling Unlock.

func (*Mutex) LockCtx

func (m *Mutex) LockCtx(ctx context.Context) bool

LockCtx is analog of Lock(), but allows to continue the try to lock only until context is done..

Returns `false` if was unable to lock (context finished before it was possible to lock).

func (*Mutex) LockCtxDo

func (m *Mutex) LockCtxDo(ctx context.Context, fn func()) (success bool)

LockCtxDo is a wrapper around LockCtx and Unlock.

See also LockDo and LockCtx.

func (*Mutex) LockDo

func (m *Mutex) LockDo(fn func())

LockDo is a wrapper around Lock and Unlock. It's a handy function to see in the call stack trace which locker where was locked. Also it's handy not to forget to unlock the locker.

func (*Mutex) LockTry

func (m *Mutex) LockTry() bool

LockTry is analog of Lock(), but it does not block if it cannot lock right away.

Returns `false` if was unable to lock.

func (*Mutex) LockTryDo

func (m *Mutex) LockTryDo(fn func()) (success bool)

LockTryDo is a wrapper around LockTry and Unlock.

See also LockDo and LockTry.

func (*Mutex) Unlock

func (m *Mutex) Unlock()

Unlock is analog of `(*sync.Mutex)`.Unlock, but it cannot be called from a routine which does not hold the lock (see `Lock`).

type RWMutex

type RWMutex struct {
	// InfiniteContext is used as the default context used on any try to lock if
	// a custom context is not set (see LockCtx/RLockCtx), but with the difference
	// if this context will be done, then it will panic with debugging information.
	//
	// To specify a context with deadline may be useful for unit tests.
	//
	// The zero-value means to use DefaultInfiniteContext.
	InfiniteContext context.Context
	// contains filtered or unexported fields
}

RWMutex is a goroutine-aware analog of sync.RWMutex, so it works the same way as sync.RWMutex, but tracks which goroutine locked it. So it could be locked multiple times with the same routine.

func (*RWMutex) Lock

func (m *RWMutex) Lock()

Lock is analog of `(*sync.RWMutex)`.Lock, but it allows one goroutine to call it and RLock multiple times without calling Unlock/RUnlock.

func (*RWMutex) LockCtx

func (m *RWMutex) LockCtx(ctx context.Context) bool

LockCtx is analog of Lock(), but allows to continue the try to lock only until context is done.

Returns `false` if was unable to lock (context finished before it was possible to lock).

func (*RWMutex) LockCtxDo

func (m *RWMutex) LockCtxDo(ctx context.Context, fn func()) (success bool)

LockCtxDo is a wrapper around LockCtx and Unlock.

See also LockDo and LockCtx.

func (*RWMutex) LockDo

func (m *RWMutex) LockDo(fn func())

LockDo is a wrapper around Lock and Unlock. It's a handy function to see in the call stack trace which locker where was locked. Also it's handy not to forget to unlock the locker.

func (*RWMutex) LockTry

func (m *RWMutex) LockTry() bool

LockTry is analog of Lock(), but it does not block if it cannot lock right away.

Returns `false` if was unable to lock.

func (*RWMutex) LockTryDo

func (m *RWMutex) LockTryDo(fn func()) (success bool)

LockTryDo is a wrapper around LockTry and Unlock.

See also LockDo and LockTry.

func (*RWMutex) RLock

func (m *RWMutex) RLock()

RLock is analog of `(*sync.RWMutex)`.RLock, but it allows one goroutine to call Lock and RLock multiple times without calling Unlock/RUnlock.

func (*RWMutex) RLockCtx

func (m *RWMutex) RLockCtx(ctx context.Context) bool

RLockCtx is analog of RLock(), but allows to continue the try to lock only until context is done.

Returns `false` if was unable to lock.

func (*RWMutex) RLockCtxDo

func (m *RWMutex) RLockCtxDo(ctx context.Context, fn func()) (success bool)

RLockCtxDo is a wrapper around RLockTry and RUnlock.

See also RLockDo and RLockCtx.

func (*RWMutex) RLockDo

func (m *RWMutex) RLockDo(fn func())

RLockDo is a wrapper around RLock and RUnlock. It's a handy function to see in the call stack trace which locker where was locked. Also it's handy not to forget to unlock the locker.

func (*RWMutex) RLockTry

func (m *RWMutex) RLockTry() bool

RLockTry is analog of RLock(), but it does not block if it cannot lock right away.

Returns `false` if was unable to lock.

func (*RWMutex) RLockTryDo

func (m *RWMutex) RLockTryDo(fn func()) (success bool)

RLockTryDo is a wrapper around RLockTry and RUnlock.

See also RLockDo and RLockTry.

func (*RWMutex) RUnlock

func (m *RWMutex) RUnlock()

RUnlock is analog of `(*sync.RWMutex)`.RUnlock, but it cannot be called from a routine which does not hold the lock (see `RLock`).

func (*RWMutex) Unlock

func (m *RWMutex) Unlock()

Unlock is analog of `(*sync.RWMutex)`.Unlock, but it cannot be called from a routine which does not hold the lock (see `Lock`).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL