Categorygithub.com/niktri/syncx
repositorypackage
1.0.6
Repository: https://github.com/niktri/syncx.git
Documentation: pkg.go.dev

# README

syncx PkgGoDev

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 of TryLocker in most benchmarks. It uses a Mutex + Atomic flag.
  • Multiple Lock() or Multiple TryLock() calls won't race with each other. However single TryLock() may race with concurrnet Lock() 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 Multiple TryLock() 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 in select loop.

HackTryLocker

RWTryLock

Usecase RWTryLock is similar to TryLock with additional TryRLock.

RWMutexTryLocker

  • RWMutexTryLocker is an implementation of RWTryLocker. It uses a Mutex + Atomic state.
  • Multiple Lock() or Multiple TryLock() or multiple TryRLock() calls won't race with each other. However single TryLock() or TryRLock may race with concurrnet Lock() or RLock() 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