Skip to content

Commit 1648eed

Browse files
authored
feat: implement priority lock (#33)
1 parent 8eee5b9 commit 1648eed

File tree

3 files changed

+148
-1
lines changed

3 files changed

+148
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ Async is a synchronization and asynchronous computation package for Go.
1717
* **Once** - An object similar to sync.Once having the Do method taking `f func() (T, error)` and returning `(T, error)`.
1818
* **Value** - An object similar to atomic.Value, but without the consistent type constraint.
1919
* **WaitGroupContext** - A WaitGroup with the `context.Context` support for graceful unblocking.
20-
* **ReentrantLock** - A Mutex that allows goroutines to enter into the lock on a resource more than once.
20+
* **ReentrantLock** - A mutex that allows goroutines to enter into the lock on a resource more than once.
21+
* **PriorityLock** - A non-reentrant mutex that allows for the specification of lock acquisition priority.
2122

2223
## Examples
2324
Can be found in the examples directory/tests.

priority_lock.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
}

priority_lock_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package async
2+
3+
import (
4+
"strconv"
5+
"strings"
6+
"testing"
7+
"time"
8+
9+
"github.com/reugn/async/internal/assert"
10+
)
11+
12+
func TestPriorityLock(t *testing.T) {
13+
p := NewPriorityLock(5)
14+
var b strings.Builder
15+
16+
p.Lock() // acquire first to make the result predictable
17+
go func() {
18+
time.Sleep(time.Millisecond)
19+
p.Unlock()
20+
}()
21+
for i := 0; i < 10; i++ {
22+
for j := 5; j > 0; j-- {
23+
go func(n int) {
24+
p.LockP(n)
25+
time.Sleep(time.Microsecond)
26+
b.WriteString(strconv.Itoa(n))
27+
p.Unlock()
28+
}(j)
29+
}
30+
}
31+
time.Sleep(20 * time.Millisecond)
32+
33+
p.Lock()
34+
result := b.String()
35+
p.Unlock()
36+
var expected strings.Builder
37+
for i := 5; i > 0; i-- {
38+
expected.WriteString(strings.Repeat(strconv.Itoa(i), 10))
39+
}
40+
assert.Equal(t, result, expected.String())
41+
}
42+
43+
func TestPriorityLock_LockRange(t *testing.T) {
44+
p := NewPriorityLock(2)
45+
var b strings.Builder
46+
p.LockP(-1)
47+
b.WriteRune('1')
48+
p.Unlock()
49+
p.LockP(2048)
50+
b.WriteRune('1')
51+
p.Unlock()
52+
assert.Equal(t, b.String(), "11")
53+
}
54+
55+
func TestPriorityLock_Panic(t *testing.T) {
56+
p := NewPriorityLock(2)
57+
p.Lock()
58+
time.Sleep(time.Nanosecond) // to silence empty critical section warning
59+
p.Unlock()
60+
assert.PanicMsgContains(t, func() { p.Unlock() }, "unlock of unlocked PriorityLock")
61+
}
62+
63+
func TestPriorityLock_Validation(t *testing.T) {
64+
assert.PanicMsgContains(t, func() { NewPriorityLock(-1) }, "nonpositive maximum priority")
65+
assert.PanicMsgContains(t, func() { NewPriorityLock(2048) }, "exceeds hard limit")
66+
}

0 commit comments

Comments
 (0)