syncx adds lock-free features to golang sync package like TryLock,RTryLock, AtomicInt, Once.IsDone.
Get syncx using:
go get github.com/niktri/syncx
- Various
TryLockimplementations - Lockfree way to acquire mutex Lock. - Various
TryRLockimplementations - 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.
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:
MutexTryLockeris best implementation ofTryLockerin 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.
import "github.com/niktri/syncx"
...
locker:=syncx.NewMutexTryLocker()
m.Lock()
fmt.Println(m.TryLock())//false
m.Unlock()
fmt.Println(m.TryLock())//true
m.Unlock()
AtomicTryLockeris 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.
ChannelTryLockeris 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 inselectloop.
HackTryLockeris implemented hacking sync.Mutex as it's first variable isstate.- It's 1000x slower than MutexTryLocker.
Usecase RWTryLock is similar to TryLock with additional TryRLock.
RWMutexTryLockeris an implementation ofRWTryLocker. It uses a Mutex + Atomic state.- Multiple
Lock()or MultipleTryLock()or multipleTryRLock()calls won't race with each other. However singleTryLock()orTryRLockmay 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.
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()
- Usecase Lockfree querying sync.Once if it is already done without doing actual work.
- golang
sync.Oncealready 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.
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
- Usecase Keeping a shared concurrent counter.
AtomicInt64self-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:
MutexInt64Plain Mutex guards variable. 3x slower than AtomicInt64.ChannelInt64. Every mutation happens in another goroutine, communicated by channel. 30x slower than 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
//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