diff --git a/README.md b/README.md index 11c75b0..abb720e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # go-lock -[![GoDoc](https://godoc.org/github.com/viney-shih/go-lock?status.svg)](https://godoc.org/github.com/viney-shih/go-lock) [![GoDev](https://img.shields.io/badge/go.dev-doc-007d9c?style=flat-square&logo=read-the-docs)](https://pkg.go.dev/github.com/viney-shih/go-lock?tab=doc) [![Build Status](https://travis-ci.com/viney-shih/go-lock.svg?branch=master)](https://travis-ci.com/github/viney-shih/go-lock) [![Go Report Card](https://goreportcard.com/badge/github.com/viney-shih/go-lock)](https://goreportcard.com/report/github.com/viney-shih/go-lock) @@ -41,39 +40,51 @@ import ( ) func main() { + // set RWMutex with CAS mechanism (CASMutex). + var rwMut lock.RWMutex = lock.NewCASMutex() // set default value - casMut := lock.NewCASMutex() count := int32(0) - casMut.Lock() + // block here + rwMut.Lock() go func() { time.Sleep(50 * time.Millisecond) fmt.Println("Now is", atomic.AddInt32(&count, 1)) // Now is 1 - casMut.Unlock() + rwMut.Unlock() }() // waiting for previous goroutine releasing the lock, and locking it again - casMut.Lock() + rwMut.Lock() fmt.Println("Now is", atomic.AddInt32(&count, 2)) // Now is 3 // TryLock without blocking - fmt.Println("Return", casMut.TryLock()) // Return false, because the lock is not released. + // Return false, because the lock is not released. + fmt.Println("Return", rwMut.TryLock()) // RTryLockWithTimeout without blocking - fmt.Println("Return", casMut.RTryLockWithTimeout(50*time.Millisecond)) // Return false, because the lock is not released. + // Return false, because the lock is not released. + fmt.Println("Return", rwMut.RTryLockWithTimeout(50*time.Millisecond)) + + // TryLockWithContext without blocking + ctx, cancel := context.WithTimeout(context.TODO(), 50*time.Millisecond) + defer cancel() + // Return false, because the lock is not released. + fmt.Println("Return", rwMut.TryLockWithContext(ctx)) + // release the lock in the end. - casMut.Unlock() + rwMut.Unlock() // Output: // Now is 1 // Now is 3 // Return false // Return false + // Return false } ``` - [More examples](./cas_test.go) -- [Full API documentation](https://godoc.org/github.com/viney-shih/go-lock) +- [Full API documentation](https://pkg.go.dev/github.com/viney-shih/go-lock?tab=doc) ## Benchmarks - Run on MacBook Pro (Retina, 15-inch, Mid 2015) 2.5 GHz Quad-Core Intel Core i7 16 GB 1600 MHz DDR3 using Go 1.15.2 diff --git a/cas.go b/cas.go index 59b4f59..05b3b2b 100644 --- a/cas.go +++ b/cas.go @@ -11,37 +11,22 @@ import ( "golang.org/x/sync/semaphore" ) -// CASMutex provides interfaces of read-write spinlock and read-write trylock with CAS mechanism. -type CASMutex interface { - // Lock acquires the write lock. - // If it is currently held by others, Lock will wait until it has a chance to acquire it. - Lock() - // TryLock attempts to acquire the write lock without blocking. - // Return false if someone is holding it now. - TryLock() bool - // TryLockWithTimeout attempts to acquire the write lock within a period of time. - // Return false if spending time is more than duration and no chance to acquire it. - TryLockWithTimeout(time.Duration) bool - // TryLockWithContext attempts to acquire the write lock, blocking until resources - // are available or ctx is done (timeout or cancellation) - TryLockWithContext(ctx context.Context) bool - // Unlock releases the write lock - Unlock() - - // RLock acquires the read lock. - // If it is currently held by others writing, RLock will wait until it has a chance to acquire it. - RLock() - // RTryLock attempts to acquire the read lock without blocking. - // Return false if someone is writing it now. - RTryLock() bool - // RTryLockWithTimeout attempts to acquire the read lock within a period of time. - // Return false if spending time is more than duration and no chance to acquire it. - RTryLockWithTimeout(time.Duration) bool - // RTryLockWithContext attempts to acquire the read lock, blocking until resources - // are available or ctx is done (timeout or cancellation) - RTryLockWithContext(ctx context.Context) bool - // RUnlock releases the read lock - RUnlock() +// CASMutex is the struct implementing RWMutex with CAS mechanism. +type CASMutex struct { + state casState + turnstile *semaphore.Weighted + + broadcastChan chan struct{} + broadcastMut sync.RWMutex +} + +// NewCASMutex returns CASMutex +func NewCASMutex() *CASMutex { + return &CASMutex{ + state: casStateNoLock, + turnstile: semaphore.NewWeighted(1), + broadcastChan: make(chan struct{}), + } } type casState int32 @@ -53,7 +38,7 @@ const ( casStateReadLock // >= 1 ) -func (m *casMutex) getState(n int32) casState { +func (m *CASMutex) getState(n int32) casState { switch st := casState(n); { case st == casStateWriteLock: fallthrough @@ -67,22 +52,14 @@ func (m *casMutex) getState(n int32) casState { } } -type casMutex struct { - state casState - turnstile *semaphore.Weighted - - broadcastChan chan struct{} - broadcastMut sync.RWMutex -} - -func (m *casMutex) listen() <-chan struct{} { +func (m *CASMutex) listen() <-chan struct{} { m.broadcastMut.RLock() defer m.broadcastMut.RUnlock() return m.broadcastChan } -func (m *casMutex) broadcast() { +func (m *CASMutex) broadcast() { newCh := make(chan struct{}) m.broadcastMut.Lock() @@ -93,7 +70,7 @@ func (m *casMutex) broadcast() { close(ch) } -func (m *casMutex) tryLock(ctx context.Context) bool { +func (m *CASMutex) tryLock(ctx context.Context) bool { for { broker := m.listen() if atomic.CompareAndSwapInt32( @@ -118,7 +95,9 @@ func (m *casMutex) tryLock(ctx context.Context) bool { } } -func (m *casMutex) TryLockWithContext(ctx context.Context) bool { +// TryLockWithContext attempts to acquire the lock, blocking until resources +// are available or ctx is done (timeout or cancellation). +func (m *CASMutex) TryLockWithContext(ctx context.Context) bool { if err := m.turnstile.Acquire(ctx, 1); err != nil { // Acquire failed due to timeout or cancellation return false @@ -129,13 +108,17 @@ func (m *casMutex) TryLockWithContext(ctx context.Context) bool { return m.tryLock(ctx) } -func (m *casMutex) Lock() { +// Lock acquires the lock. +// If it is currently held by others, Lock will wait until it has a chance to acquire it. +func (m *CASMutex) Lock() { ctx := context.Background() m.TryLockWithContext(ctx) } -func (m *casMutex) TryLock() bool { +// TryLock attempts to acquire the lock without blocking. +// Return false if someone is holding it now. +func (m *CASMutex) TryLock() bool { if !m.turnstile.TryAcquire(1) { return false } @@ -145,14 +128,17 @@ func (m *casMutex) TryLock() bool { return m.tryLock(nil) } -func (m *casMutex) TryLockWithTimeout(duration time.Duration) bool { +// TryLockWithTimeout attempts to acquire the lock within a period of time. +// Return false if spending time is more than duration and no chance to acquire it. +func (m *CASMutex) TryLockWithTimeout(duration time.Duration) bool { ctx, cancel := context.WithTimeout(context.Background(), duration) defer cancel() return m.TryLockWithContext(ctx) } -func (m *casMutex) Unlock() { +// Unlock releases the lock. +func (m *CASMutex) Unlock() { if ok := atomic.CompareAndSwapInt32( (*int32)(unsafe.Pointer(&m.state)), int32(casStateWriteLock), @@ -164,7 +150,7 @@ func (m *casMutex) Unlock() { m.broadcast() } -func (m *casMutex) rTryLock(ctx context.Context) bool { +func (m *CASMutex) rTryLock(ctx context.Context) bool { for { broker := m.listen() n := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.state))) @@ -203,7 +189,9 @@ func (m *casMutex) rTryLock(ctx context.Context) bool { } } -func (m *casMutex) RTryLockWithContext(ctx context.Context) bool { +// RTryLockWithContext attempts to acquire the read lock, blocking until resources +// are available or ctx is done (timeout or cancellation). +func (m *CASMutex) RTryLockWithContext(ctx context.Context) bool { if err := m.turnstile.Acquire(ctx, 1); err != nil { // Acquire failed due to timeout or cancellation return false @@ -214,13 +202,17 @@ func (m *casMutex) RTryLockWithContext(ctx context.Context) bool { return m.rTryLock(ctx) } -func (m *casMutex) RLock() { +// RLock acquires the read lock. +// If it is currently held by others writing, RLock will wait until it has a chance to acquire it. +func (m *CASMutex) RLock() { ctx := context.Background() m.RTryLockWithContext(ctx) } -func (m *casMutex) RTryLock() bool { +// RTryLock attempts to acquire the read lock without blocking. +// Return false if someone is writing it now. +func (m *CASMutex) RTryLock() bool { if !m.turnstile.TryAcquire(1) { return false } @@ -230,14 +222,17 @@ func (m *casMutex) RTryLock() bool { return m.rTryLock(nil) } -func (m *casMutex) RTryLockWithTimeout(duration time.Duration) bool { +// RTryLockWithTimeout attempts to acquire the read lock within a period of time. +// Return false if spending time is more than duration and no chance to acquire it. +func (m *CASMutex) RTryLockWithTimeout(duration time.Duration) bool { ctx, cancel := context.WithTimeout(context.Background(), duration) defer cancel() return m.RTryLockWithContext(ctx) } -func (m *casMutex) RUnlock() { +// RUnlock releases the read lock. +func (m *CASMutex) RUnlock() { n := atomic.AddInt32((*int32)(unsafe.Pointer(&m.state)), -1) switch m.getState(n) { case casStateUndefined, casStateWriteLock: @@ -247,11 +242,13 @@ func (m *casMutex) RUnlock() { } } -// NewCASMutex returns CASMutex lock -func NewCASMutex() CASMutex { - return &casMutex{ - state: casStateNoLock, - turnstile: semaphore.NewWeighted(1), - broadcastChan: make(chan struct{}), - } +// RLocker returns a Locker interface that implements the Lock and Unlock methods +// by calling CASMutex.RLock and CASMutex.RUnlock. +func (m *CASMutex) RLocker() sync.Locker { + return (*rlocker)(m) } + +type rlocker CASMutex + +func (r *rlocker) Lock() { (*CASMutex)(r).RLock() } +func (r *rlocker) Unlock() { (*CASMutex)(r).RUnlock() } diff --git a/chan.go b/chan.go index 22639ef..355f32d 100644 --- a/chan.go +++ b/chan.go @@ -5,37 +5,32 @@ import ( "time" ) -// ChanMutex provides interfaces of spinlock and trylock implemented by channel. -type ChanMutex interface { - // Lock acquires the lock. - // If it is currently held by others, Lock will wait until it has a chance to acquire it. - Lock() - // TryLock attempts to acquire the lock without blocking. - // Return false if someone is holding it now. - TryLock() bool - // TryLockWithTimeout attempts to acquire the lock within a period of time. - // Return false if spending time is more than duration and no chance to acquire it. - TryLockWithTimeout(time.Duration) bool - // TryLockWithContext attempts to acquire the lock, blocking until resources - // are available or ctx is done (timeout or cancellation) - TryLockWithContext(ctx context.Context) bool - // Unlock releases the lock - Unlock() +// ChanMutex is the struct implementing Mutex by channel. +type ChanMutex struct { + lockChan chan struct{} } -type chanMutex struct { - lockChan chan struct{} +// NewChanMutex returns ChanMutex. +func NewChanMutex() *ChanMutex { + return &ChanMutex{ + lockChan: make(chan struct{}, 1), + } } -func (m *chanMutex) Lock() { +// Lock acquires the lock. +// If it is currently held by others, Lock will wait until it has a chance to acquire it. +func (m *ChanMutex) Lock() { m.lockChan <- struct{}{} } -func (m *chanMutex) Unlock() { +// Unlock releases the lock. +func (m *ChanMutex) Unlock() { <-m.lockChan } -func (m *chanMutex) TryLock() bool { +// TryLock attempts to acquire the lock without blocking. +// Return false if someone is holding it now. +func (m *ChanMutex) TryLock() bool { select { case m.lockChan <- struct{}{}: return true @@ -44,7 +39,9 @@ func (m *chanMutex) TryLock() bool { } } -func (m *chanMutex) TryLockWithContext(ctx context.Context) bool { +// TryLockWithContext attempts to acquire the lock, blocking until resources +// are available or ctx is done (timeout or cancellation). +func (m *ChanMutex) TryLockWithContext(ctx context.Context) bool { select { case m.lockChan <- struct{}{}: return true @@ -54,16 +51,11 @@ func (m *chanMutex) TryLockWithContext(ctx context.Context) bool { } } -func (m *chanMutex) TryLockWithTimeout(duration time.Duration) bool { +// TryLockWithTimeout attempts to acquire the lock within a period of time. +// Return false if spending time is more than duration and no chance to acquire it. +func (m *ChanMutex) TryLockWithTimeout(duration time.Duration) bool { ctx, cancel := context.WithTimeout(context.Background(), duration) defer cancel() return m.TryLockWithContext(ctx) } - -// NewChanMutex returns ChanMutex lock -func NewChanMutex() ChanMutex { - return &chanMutex{ - lockChan: make(chan struct{}, 1), - } -} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..3f1d7b0 --- /dev/null +++ b/example_test.go @@ -0,0 +1,53 @@ +package lock_test + +import ( + "context" + "fmt" + "sync/atomic" + "time" + + "github.com/viney-shih/go-lock" +) + +func ExampleChanMutex() { + // set RWMutex with CAS mechanism (CASMutex). + var rwMut lock.RWMutex = lock.NewCASMutex() + // set default value + count := int32(0) + + // block here + rwMut.Lock() + go func() { + time.Sleep(50 * time.Millisecond) + fmt.Println("Now is", atomic.AddInt32(&count, 1)) // Now is 1 + rwMut.Unlock() + }() + + // waiting for previous goroutine releasing the lock, and locking it again + rwMut.Lock() + fmt.Println("Now is", atomic.AddInt32(&count, 2)) // Now is 3 + + // TryLock without blocking + // Return false, because the lock is not released. + fmt.Println("Return", rwMut.TryLock()) + + // RTryLockWithTimeout without blocking + // Return false, because the lock is not released. + fmt.Println("Return", rwMut.RTryLockWithTimeout(50*time.Millisecond)) + + // TryLockWithContext without blocking + ctx, cancel := context.WithTimeout(context.TODO(), 50*time.Millisecond) + defer cancel() + // Return false, because the lock is not released. + fmt.Println("Return", rwMut.TryLockWithContext(ctx)) + + // release the lock in the end. + rwMut.Unlock() + + // Output: + // Now is 1 + // Now is 3 + // Return false + // Return false + // Return false +} diff --git a/lock.go b/lock.go new file mode 100644 index 0000000..4169635 --- /dev/null +++ b/lock.go @@ -0,0 +1,53 @@ +// Package go-lock is a Golang library implementing an effcient read-write lock with the following built-in mechanism: +// - Mutex with timeout mechanism +// - Trylock +// - No-starve read-write solution +package lock + +import ( + "context" + "sync" + "time" +) + +// Mutex is a mutual exclusion lock, and must not be copied after first use. +type Mutex interface { + // Lock acquires the lock. + // If it is currently held by others, Lock will wait until it has a chance to acquire it. + Lock() + // TryLock attempts to acquire the lock without blocking. + // Return false if someone is holding it now. + TryLock() bool + // TryLockWithTimeout attempts to acquire the lock within a period of time. + // Return false if spending time is more than duration and no chance to acquire it. + TryLockWithTimeout(time.Duration) bool + // TryLockWithContext attempts to acquire the lock, blocking until resources + // are available or ctx is done (timeout or cancellation). + TryLockWithContext(ctx context.Context) bool + // Unlock releases the lock. + Unlock() +} + +// RWMutex is a reader/writer mutual exclusion lock. The lock can be held by an arbitrary number of +// readers or a single writer. It must not be copied after first use. +type RWMutex interface { + Mutex + + // RLock acquires the read lock. + // If it is currently held by others writing, RLock will wait until it has a chance to acquire it. + RLock() + // RTryLock attempts to acquire the read lock without blocking. + // Return false if someone is writing it now. + RTryLock() bool + // RTryLockWithTimeout attempts to acquire the read lock within a period of time. + // Return false if spending time is more than duration and no chance to acquire it. + RTryLockWithTimeout(time.Duration) bool + // RTryLockWithContext attempts to acquire the read lock, blocking until resources + // are available or ctx is done (timeout or cancellation). + RTryLockWithContext(ctx context.Context) bool + // RUnlock releases the read lock. + RUnlock() + // RLocker returns a Locker interface that implements the Lock and Unlock methods + // by calling rw.RLock and rw.RUnlock. + RLocker() sync.Locker +}