|
| 1 | +package async |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "sync" |
| 6 | +) |
| 7 | + |
| 8 | +const priorityLimit = 1024 |
| 9 | + |
| 10 | +// PriorityLock is a non-reentrant mutex that allows specifying a priority |
| 11 | +// level when acquiring the lock. It extends the standard sync.Locker interface |
| 12 | +// with an additional locking method, LockP, which takes a priority level as an |
| 13 | +// argument. |
| 14 | +// |
| 15 | +// The current implementation may cause starvation for lower priority |
| 16 | +// lock requests. |
| 17 | +type PriorityLock struct { |
| 18 | + sem []chan struct{} |
| 19 | + max int |
| 20 | +} |
| 21 | + |
| 22 | +var _ sync.Locker = (*PriorityLock)(nil) |
| 23 | + |
| 24 | +// NewPriorityLock instantiates and returns a new PriorityLock, specifying the |
| 25 | +// maximum priority level that can be used in the LockP method. It panics if |
| 26 | +// the maximum priority level is non-positive or exceeds the hard limit. |
| 27 | +func NewPriorityLock(maxPriority int) *PriorityLock { |
| 28 | + if maxPriority < 1 { |
| 29 | + panic(fmt.Errorf("nonpositive maximum priority: %d", maxPriority)) |
| 30 | + } |
| 31 | + if maxPriority > priorityLimit { |
| 32 | + panic(fmt.Errorf("maximum priority %d exceeds hard limit of %d", |
| 33 | + maxPriority, priorityLimit)) |
| 34 | + } |
| 35 | + sem := make([]chan struct{}, maxPriority+1) |
| 36 | + sem[0] = make(chan struct{}, 1) |
| 37 | + sem[0] <- struct{}{} |
| 38 | + for i := 1; i <= maxPriority; i++ { |
| 39 | + sem[i] = make(chan struct{}) |
| 40 | + } |
| 41 | + return &PriorityLock{ |
| 42 | + sem: sem, |
| 43 | + max: maxPriority, |
| 44 | + } |
| 45 | +} |
| 46 | + |
| 47 | +// Lock will block the calling goroutine until it acquires the lock, using |
| 48 | +// the highest available priority. |
| 49 | +func (pl *PriorityLock) Lock() { |
| 50 | + pl.LockP(pl.max) |
| 51 | +} |
| 52 | + |
| 53 | +// LockP blocks the calling goroutine until it acquires the lock. Requests with |
| 54 | +// higher priorities acquire the lock first. If the provided priority is |
| 55 | +// outside the valid range, it will be assigned the boundary value. |
| 56 | +func (pl *PriorityLock) LockP(priority int) { |
| 57 | + switch { |
| 58 | + case priority < 1: |
| 59 | + priority = 1 |
| 60 | + case priority > pl.max: |
| 61 | + priority = pl.max |
| 62 | + } |
| 63 | + select { |
| 64 | + case <-pl.sem[priority]: |
| 65 | + case <-pl.sem[0]: |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +// Unlock releases the previously acquired lock. |
| 70 | +// It will panic if the lock is already unlocked. |
| 71 | +func (pl *PriorityLock) Unlock() { |
| 72 | + for i := pl.max; i >= 0; i-- { |
| 73 | + select { |
| 74 | + case pl.sem[i] <- struct{}{}: |
| 75 | + return |
| 76 | + default: |
| 77 | + } |
| 78 | + } |
| 79 | + panic("async: unlock of unlocked PriorityLock") |
| 80 | +} |
0 commit comments