Skip to content

Commit 7a045ec

Browse files
committed
Locker v2
Breaking changes: - The minimum Go version is go1.19 - The result parameter to (*Locker).Unlock() has been dropped. Instead the application will crash with the same fatal run-time error as calling (sync.Mutex).Unlock() on an unlocked mutex. - ErrNoSuchLock has been removed. - New() has been removed. The zero value is valid. Modernize the codebase by taking advantage of Go 1.19 atomics. Add an RWLocker for read-write locks. Add sync.Locker interface adapters for Locker and RWLocker. Signed-off-by: Cory Snider <csnider@mirantis.com>
1 parent 281af2d commit 7a045ec

File tree

5 files changed

+310
-86
lines changed

5 files changed

+310
-86
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
module github.com/moby/locker
1+
module github.com/moby/locker/v2
22

3-
go 1.13
3+
go 1.19

locker.go

Lines changed: 36 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -14,61 +14,24 @@ waiting for the lock.
1414
package locker
1515

1616
import (
17-
"errors"
1817
"sync"
1918
"sync/atomic"
2019
)
2120

22-
// ErrNoSuchLock is returned when the requested lock does not exist
23-
var ErrNoSuchLock = errors.New("no such lock")
24-
25-
// Locker provides a locking mechanism based on the passed in reference name
21+
// Locker provides a locking mechanism based on the passed in reference name.
22+
// The zero value is a valid Locker.
2623
type Locker struct {
2724
mu sync.Mutex
2825
locks map[string]*lockCtr
2926
}
3027

3128
// lockCtr is used by Locker to represent a lock with a given name.
3229
type lockCtr struct {
33-
mu sync.Mutex
34-
// waiters is the number of waiters waiting to acquire the lock
35-
// this is int32 instead of uint32 so we can add `-1` in `dec()`
36-
waiters int32
37-
}
38-
39-
// inc increments the number of waiters waiting for the lock
40-
func (l *lockCtr) inc() {
41-
atomic.AddInt32(&l.waiters, 1)
42-
}
43-
44-
// dec decrements the number of waiters waiting on the lock
45-
func (l *lockCtr) dec() {
46-
atomic.AddInt32(&l.waiters, -1)
47-
}
48-
49-
// count gets the current number of waiters
50-
func (l *lockCtr) count() int32 {
51-
return atomic.LoadInt32(&l.waiters)
52-
}
53-
54-
// Lock locks the mutex
55-
func (l *lockCtr) Lock() {
56-
l.mu.Lock()
30+
sync.Mutex
31+
waiters atomic.Int32 // Number of callers waiting to acquire the lock
5732
}
5833

59-
// Unlock unlocks the mutex
60-
func (l *lockCtr) Unlock() {
61-
l.mu.Unlock()
62-
}
63-
64-
// New creates a new Locker
65-
func New() *Locker {
66-
return &Locker{
67-
locks: make(map[string]*lockCtr),
68-
}
69-
}
70-
71-
// Lock locks a mutex with the given name. If it doesn't exist, one is created
34+
// Lock locks a mutex with the given name.
7235
func (l *Locker) Lock(name string) {
7336
l.mu.Lock()
7437
if l.locks == nil {
@@ -81,32 +44,49 @@ func (l *Locker) Lock(name string) {
8144
l.locks[name] = nameLock
8245
}
8346

84-
// increment the nameLock waiters while inside the main mutex
85-
// this makes sure that the lock isn't deleted if `Lock` and `Unlock` are called concurrently
86-
nameLock.inc()
47+
// Increment the nameLock waiters while inside the main mutex.
48+
// This makes sure that the lock isn't deleted if `Lock` and `Unlock` are called concurrently.
49+
nameLock.waiters.Add(1)
8750
l.mu.Unlock()
8851

89-
// Lock the nameLock outside the main mutex so we don't block other operations
90-
// once locked then we can decrement the number of waiters for this lock
52+
// Lock the nameLock outside the main mutex so we don't block other operations.
53+
// Once locked then we can decrement the number of waiters for this lock.
9154
nameLock.Lock()
92-
nameLock.dec()
55+
nameLock.waiters.Add(-1)
9356
}
9457

95-
// Unlock unlocks the mutex with the given name
96-
// If the given lock is not being waited on by any other callers, it is deleted
97-
func (l *Locker) Unlock(name string) error {
58+
// Unlock unlocks the mutex with the given name.
59+
// It is a run-time error if the named lock is not locked on entry to Unlock.
60+
func (l *Locker) Unlock(name string) {
9861
l.mu.Lock()
62+
defer l.mu.Unlock()
9963
nameLock, exists := l.locks[name]
10064
if !exists {
101-
l.mu.Unlock()
102-
return ErrNoSuchLock
65+
// Generate an un-recover()-able error without reaching into runtime internals.
66+
(&sync.Mutex{}).Unlock()
10367
}
10468

105-
if nameLock.count() == 0 {
69+
if nameLock.waiters.Load() <= 0 {
10670
delete(l.locks, name)
10771
}
10872
nameLock.Unlock()
73+
}
10974

110-
l.mu.Unlock()
111-
return nil
75+
type nameLocker struct {
76+
l *Locker
77+
name string
78+
}
79+
80+
// Locker returns a [sync.Locker] interface that implements
81+
// the [sync.Locker.Lock] and [sync.Locker.Unlock] methods
82+
// by calling l.Lock(name) and l.Unlock(name).
83+
func (l *Locker) Locker(name string) sync.Locker {
84+
return nameLocker{l: l, name: name}
85+
}
86+
87+
func (n nameLocker) Lock() {
88+
n.l.Lock(n.name)
89+
}
90+
func (n nameLocker) Unlock() {
91+
n.l.Unlock(n.name)
11292
}

locker_test.go

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,13 @@ import (
88
"time"
99
)
1010

11-
func TestLockCounter(t *testing.T) {
12-
l := &lockCtr{}
13-
l.inc()
14-
15-
if l.waiters != 1 {
16-
t.Fatal("counter inc failed")
17-
}
18-
19-
l.dec()
20-
if l.waiters != 0 {
21-
t.Fatal("counter dec failed")
22-
}
23-
}
24-
2511
func TestLockerLock(t *testing.T) {
26-
l := New()
12+
var l Locker
2713
l.Lock("test")
2814
ctr := l.locks["test"]
2915

30-
if ctr.count() != 0 {
31-
t.Fatalf("expected waiters to be 0, got :%d", ctr.waiters)
16+
if w := ctr.waiters.Load(); w != 0 {
17+
t.Fatalf("expected waiters to be 0, got %d", w)
3218
}
3319

3420
chDone := make(chan struct{})
@@ -40,7 +26,7 @@ func TestLockerLock(t *testing.T) {
4026
chWaiting := make(chan struct{})
4127
go func() {
4228
for range time.Tick(1 * time.Millisecond) {
43-
if ctr.count() == 1 {
29+
if ctr.waiters.Load() == 1 {
4430
close(chWaiting)
4531
break
4632
}
@@ -59,23 +45,21 @@ func TestLockerLock(t *testing.T) {
5945
default:
6046
}
6147

62-
if err := l.Unlock("test"); err != nil {
63-
t.Fatal(err)
64-
}
48+
l.Unlock("test")
6549

6650
select {
6751
case <-chDone:
6852
case <-time.After(3 * time.Second):
6953
t.Fatalf("lock should have completed")
7054
}
7155

72-
if ctr.count() != 0 {
73-
t.Fatalf("expected waiters to be 0, got: %d", ctr.count())
56+
if w := ctr.waiters.Load(); w != 0 {
57+
t.Fatalf("expected waiters to be 0, got %d", w)
7458
}
7559
}
7660

7761
func TestLockerUnlock(t *testing.T) {
78-
l := New()
62+
var l Locker
7963

8064
l.Lock("test")
8165
l.Unlock("test")
@@ -94,7 +78,7 @@ func TestLockerUnlock(t *testing.T) {
9478
}
9579

9680
func TestLockerConcurrency(t *testing.T) {
97-
l := New()
81+
var l Locker
9882

9983
var wg sync.WaitGroup
10084
for i := 0; i <= 10000; i++ {
@@ -126,15 +110,15 @@ func TestLockerConcurrency(t *testing.T) {
126110
}
127111

128112
func BenchmarkLocker(b *testing.B) {
129-
l := New()
113+
var l Locker
130114
for i := 0; i < b.N; i++ {
131115
l.Lock("test")
132116
l.Unlock("test")
133117
}
134118
}
135119

136120
func BenchmarkLockerParallel(b *testing.B) {
137-
l := New()
121+
var l Locker
138122
b.SetParallelism(128)
139123
b.RunParallel(func(pb *testing.PB) {
140124
for pb.Next() {
@@ -145,7 +129,7 @@ func BenchmarkLockerParallel(b *testing.B) {
145129
}
146130

147131
func BenchmarkLockerMoreKeys(b *testing.B) {
148-
l := New()
132+
var l Locker
149133
var keys []string
150134
for i := 0; i < 64; i++ {
151135
keys = append(keys, strconv.Itoa(i))

rwlocker.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package locker
2+
3+
import (
4+
"sync"
5+
"sync/atomic"
6+
)
7+
8+
// Locker provides a read-write lock based on the passed in reference name.
9+
// The zero value is a valid RWLocker.
10+
type RWLocker struct {
11+
mu sync.Mutex
12+
locks map[string]*rwlockCtr
13+
}
14+
15+
// rwlockCtr is used by RWLocker to represent a lock with a given name.
16+
type rwlockCtr struct {
17+
sync.RWMutex
18+
waiters atomic.Int32 // Number of callers waiting to acquire the lock
19+
}
20+
21+
func (l *RWLocker) doLock(name string, op func(*sync.RWMutex)) {
22+
l.mu.Lock()
23+
if l.locks == nil {
24+
l.locks = make(map[string]*rwlockCtr)
25+
}
26+
27+
nameLock, exists := l.locks[name]
28+
if !exists {
29+
nameLock = &rwlockCtr{}
30+
l.locks[name] = nameLock
31+
}
32+
33+
// Increment the nameLock waiters while inside the main mutex.
34+
// This makes sure that the lock isn't deleted if `doLock` and `doUnlock` are called concurrently.
35+
nameLock.waiters.Add(1)
36+
l.mu.Unlock()
37+
38+
// Lock the nameLock outside the main mutex so we don't block other operations.
39+
// Once locked then we can decrement the number of waiters for this lock.
40+
op(&nameLock.RWMutex)
41+
nameLock.waiters.Add(-1)
42+
}
43+
44+
// Lock locks the mutex with the given name for writing.
45+
func (l *RWLocker) Lock(name string) {
46+
l.doLock(name, (*sync.RWMutex).Lock)
47+
}
48+
49+
// RLock locks the mutex with the given name for reading.
50+
func (l *RWLocker) RLock(name string) {
51+
l.doLock(name, (*sync.RWMutex).RLock)
52+
}
53+
54+
func (l *RWLocker) doUnlock(name string, op func(*sync.RWMutex)) {
55+
l.mu.Lock()
56+
defer l.mu.Unlock()
57+
nameLock, exists := l.locks[name]
58+
if !exists {
59+
// Generate an un-recover()-able error without reaching into runtime internals.
60+
op(&sync.RWMutex{})
61+
}
62+
63+
if nameLock.waiters.Load() <= 0 {
64+
delete(l.locks, name)
65+
}
66+
op(&nameLock.RWMutex)
67+
}
68+
69+
// Unlock unlocks the mutex with the given name.
70+
// It is a run-time error if the named lock is not locked for writing on entry to Unlock.
71+
func (l *RWLocker) Unlock(name string) {
72+
l.doUnlock(name, (*sync.RWMutex).Unlock)
73+
}
74+
75+
// RUnlock unlocks the mutex with the given name for reading.
76+
// It is a run-time error if the named lock is not locked for reading on entry to RUnlock.
77+
func (l *RWLocker) RUnlock(name string) {
78+
l.doUnlock(name, (*sync.RWMutex).RUnlock)
79+
}
80+
81+
// Locker returns a [sync.Locker] interface that implements
82+
// the [sync.Locker.Lock] and [sync.Locker.Unlock] methods
83+
// by calling l.Lock(name) and l.Unlock(name).
84+
func (l *RWLocker) Locker(name string) sync.Locker {
85+
return nameRWLocker{l: l, name: name}
86+
}
87+
88+
// RLocker returns a [sync.Locker] interface that implements
89+
// the [sync.Locker.Lock] and [sync.Locker.Unlock] methods
90+
// by calling l.RLock(name) and l.RUnlock(name).
91+
func (l *RWLocker) RLocker(name string) sync.Locker {
92+
return nameRLocker{l: l, name: name}
93+
}
94+
95+
type nameRWLocker struct {
96+
l *RWLocker
97+
name string
98+
}
99+
type nameRLocker nameRWLocker
100+
101+
func (n nameRWLocker) Lock() {
102+
n.l.Lock(n.name)
103+
}
104+
func (n nameRWLocker) Unlock() {
105+
n.l.Unlock(n.name)
106+
}
107+
108+
func (n nameRLocker) Lock() {
109+
n.l.RLock(n.name)
110+
}
111+
func (n nameRLocker) Unlock() {
112+
n.l.RUnlock(n.name)
113+
}

0 commit comments

Comments
 (0)