/* Package locker provides a mechanism for creating finer-grained locking to help free up more global locks to handle other tasks. The implementation looks close to a sync.Mutex, however the user must provide a reference to use to refer to the underlying lock when locking and unlocking, and unlock may generate an error. If a lock with a given name does not exist when `Lock` is called, one is created. Lock references are automatically cleaned up on `Unlock` if nothing else is waiting for the lock. */ package locker import ( "errors" "sync" "sync/atomic" ) // ErrNoSuchLock is returned when the requested lock does not exist var ErrNoSuchLock = errors.New("no such lock") // Locker provides a locking mechanism based on the passed in reference name type Locker struct { mu sync.Mutex locks map[string]*lockCtr } // lockCtr is used by Locker to represent a lock with a given name. type lockCtr struct { mu sync.Mutex // waiters is the number of waiters waiting to acquire the lock waiters uint32 } // inc increments the number of waiters waiting for the lock func (l *lockCtr) inc() { atomic.AddUint32(&l.waiters, 1) } // dec decrements the number of waiters wating on the lock func (l *lockCtr) dec() { atomic.AddUint32(&l.waiters, ^uint32(l.waiters-1)) } // count gets the current number of waiters func (l *lockCtr) count() uint32 { return atomic.LoadUint32(&l.waiters) } // Lock locks the mutex func (l *lockCtr) Lock() { l.mu.Lock() } // Unlock unlocks the mutex func (l *lockCtr) Unlock() { l.mu.Unlock() } // New creates a new Locker func New() *Locker { return &Locker{ locks: make(map[string]*lockCtr), } } // Lock locks a mutex with the given name. If it doesn't exist, one is created func (l *Locker) Lock(name string) { l.mu.Lock() if l.locks == nil { l.locks = make(map[string]*lockCtr) } nameLock, exists := l.locks[name] if !exists { nameLock = &lockCtr{} l.locks[name] = nameLock } // increment the nameLock waiters while inside the main mutex // this makes sure that the lock isn't deleted if `Lock` and `Unlock` are called concurrently nameLock.inc() l.mu.Unlock() // Lock the nameLock outside the main mutex so we don't block other operations // once locked then we can decrement the number of waiters for this lock nameLock.Lock() nameLock.dec() } // Unlock unlocks the mutex with the given name // If the given lock is not being waited on by any other callers, it is deleted func (l *Locker) Unlock(name string) error { l.mu.Lock() nameLock, exists := l.locks[name] if !exists { l.mu.Unlock() return ErrNoSuchLock } if nameLock.count() == 0 { delete(l.locks, name) } nameLock.Unlock() l.mu.Unlock() return nil }