# README
syncx 
syncx adds lock-free features to golang sync package like TryLock
,RTryLock
, AtomicInt
, Once.IsDone
.
Get syncx using:
go get github.com/niktri/syncx
Features
- Various
TryLock
implementations - Lockfree way to acquire mutex Lock. - Various
TryRLock
implementations - Lockfree way to acquire RWMutex Lock. - Various lock-free functions like
Once.IsDone()
,Locker.IsLocked()
extending golang/sync AtomicInt64
- Safer alternative of sync/atomic, avoiding accidental unsafe access of shared variable & self-documenting its purpose.
TryLock
Usecase Any usecase to obtain lock without blocking. e.g. Cache cleanup thread wants to clean expired entries, but not if refresher thread is already busy.
TryLock
is Much discussed in golang community & it seems(may be rightly so) golang won't implement it.
We have 4 implementations of TryLocker interface:
MutexTryLocker
MutexTryLocker
is best implementation ofTryLocker
in most benchmarks. It uses a Mutex + Atomic flag.- Multiple
Lock()
or MultipleTryLock()
calls won't race with each other. However singleTryLock()
may race with concurrnetLock()
calls. In this scenario race will be resolved on best effort basis. This is why it's TryLock is psuedo-non-blocking. - It's Lock-Unlock performance is very close(~8%) to Plain sync.Mutex.
Example MutexTryLocker
import "github.com/niktri/syncx"
...
locker:=syncx.NewMutexTryLocker()
m.Lock()
fmt.Println(m.TryLock())//false
m.Unlock()
fmt.Println(m.TryLock())//true
m.Unlock()
AtomicTryLocker
AtomicTryLocker
is similar to MutexTryLocker using only atomic flag without Mutex.- Multiple
Lock()
or MultipleTryLock()
calls will race with each other, race will be resolved on best effort basis. - It's Lock-Unlock performance is very close(~8%) to Plain sync.Mutex.
ChannelTryLocker
ChannelTryLocker
is implemented using go channels.- It does not have live-lock issue of MutexTryLocker, but Lock() & TryLock() are 3x & 100x slower.
- ChannelTryLocker efficiently implements
TryLockWithTimeout
, waiting for channel or time simultaneously inselect
loop.
HackTryLocker
HackTryLocker
is implemented hacking sync.Mutex as it's first variable isstate
.- It's 1000x slower than MutexTryLocker.
RWTryLock
Usecase RWTryLock
is similar to TryLock with additional TryRLock
.
RWMutexTryLocker
RWMutexTryLocker
is an implementation ofRWTryLocker
. It uses a Mutex + Atomic state.- Multiple
Lock()
or MultipleTryLock()
or multipleTryRLock()
calls won't race with each other. However singleTryLock()
orTryRLock
may race with concurrnetLock()
orRLock()
calls. In this scenario race will be resolved on best effort basis. This is why it's TryLock is psuedo-non-blocking. - It's Lock-Unlock performance is very close(~8%) to Plain sync.RWMutex.
Example MutexRWTryLocker
m := syncx.NewMutexRWTryLocker()
m.Lock()
fmt.Println(m.TryLock()) //false
fmt.Println(m.TryRLock()) //false
m.Unlock()
fmt.Println(m.TryLock()) //true
fmt.Println(m.TryRLock()) //false
m.Unlock()
fmt.Println(m.TryRLock()) //true
fmt.Println(m.TryLock()) //false
fmt.Println(m.TryRLock()) //true
fmt.Println(m.TryRLock()) //true
fmt.Println(m.TryLock()) //false
m.RUnlock()
m.RUnlock()
m.RUnlock()
fmt.Println(m.TryLock()) //true
m.Unlock()
Once.IsDone()
- Usecase Lockfree querying sync.Once if it is already done without doing actual work.
- golang
sync.Once
already keeps a flag if it's done or not. It does not expose it. - syncx.Once.IsDone() just exposes this flag. It's simpler to use without duplicating flag and messing with atomics.
- As discussed here, it's concluded that there aren't enough usecases to include to standard library.
Example Once.IsDone()
once := syncx.Once{}
fmt.Println(once.IsDone()) //false
go once.Do(func() {
time.Sleep(3 * time.Second)
})
fmt.Println(once.IsDone()) //false
time.Sleep(1 * time.Second)
fmt.Println(once.IsDone()) //false
time.Sleep(5 * time.Second)
fmt.Println(once.IsDone()) //true
AtomicInt64
- Usecase Keeping a shared concurrent counter.
AtomicInt64
self-documents its purpose & protects accidental unsafe access to shared counter compared to sync.atomic CAS ops.- It's just a plain int64 type with safe convenient functions:
Get, Set, Incr, Decr, Add, Sub, SetIf, String, IncrString, DecrString
. - It implements Stringer. So it conveniently returns live value if stored in a repo. e.g. If stored as a field in logrus.WithField, it will always log live value.
- There are 2 more implementations of counters only for benchmarking purpose:
MutexInt64
Plain Mutex guards variable. 3x slower than AtomicInt64.ChannelInt64
. Every mutation happens in another goroutine, communicated by channel. 30x slower than AtomicInt64
Example AtomicInt64
a := syncx.NewAtomicInt64(0)
a.Set(100)
a.Incr()
a.Add(50)
a.Sub(150)
fmt.Println(a.Decr()) //0
fmt.Println(a.String()) //0
wg := sync.WaitGroup{}
f := func(incr int64) {
for i := 0; i < 100000; i++ {
a.Add(incr)
}
wg.Done()
}
wg.Add(2)
go f(1)
go f(-1)
wg.Wait()
fmt.Println(a) //0
Example AtomicInt64 live logging
//On startup
globalCounter := syncx.NewAtomicInt64(0)
//On Every Request
log:=logrus.WithField("counter", globalCounter)
a.Incr()
processRequest(contextWithLogger)
a.Decr()
//Deep down stack inside processRequest()
log.Log("Connected to Service") //Prints live globalCounter value