Skip to content

Commit 63fe334

Browse files
committed
quic: gate and queue synchronization primitives
Add a form of monitor (in the sense of the synchronization primitive) for controlling access to queues and streams. We call this a "gate". A gate acts as a mutex and condition variable with one bit of state. A gate may be locked and unlocked. Lock operations may optionally block on the gate condition being set. Unlock operations always record the new value of the condition. Gates play nicely with contexts. Unlike traditional condition variables, gates do not suffer from spurious wakeups: A goroutine waiting for a gate condition is not woken before the condition is set. Gates are inspired by the queue design from Bryan Mills's talk, Rethinking Classical Concurrency Patterns. Add a queue implemented with a gate. For golang/go#58547 Change-Id: Ibec6d1f29a2c03a7184fca7392ed5639f96b6485 Reviewed-on: https://go-review.googlesource.com/c/net/+/513955 TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]> Run-TryBot: Damien Neil <[email protected]>
1 parent bd8ac9e commit 63fe334

File tree

4 files changed

+370
-0
lines changed

4 files changed

+370
-0
lines changed

internal/quic/gate.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.21
6+
7+
package quic
8+
9+
import "context"
10+
11+
// An gate is a monitor (mutex + condition variable) with one bit of state.
12+
//
13+
// The condition may be either set or unset.
14+
// Lock operations may be unconditional, or wait for the condition to be set.
15+
// Unlock operations record the new state of the condition.
16+
type gate struct {
17+
// When unlocked, exactly one of set or unset contains a value.
18+
// When locked, neither chan contains a value.
19+
set chan struct{}
20+
unset chan struct{}
21+
}
22+
23+
func newGate() gate {
24+
g := gate{
25+
set: make(chan struct{}, 1),
26+
unset: make(chan struct{}, 1),
27+
}
28+
g.unset <- struct{}{}
29+
return g
30+
}
31+
32+
// lock acquires the gate unconditionally.
33+
// It reports whether the condition is set.
34+
func (g *gate) lock() (set bool) {
35+
select {
36+
case <-g.set:
37+
return true
38+
case <-g.unset:
39+
return false
40+
}
41+
}
42+
43+
// waitAndLock waits until the condition is set before acquiring the gate.
44+
func (g *gate) waitAndLock() {
45+
<-g.set
46+
}
47+
48+
// waitAndLockContext waits until the condition is set before acquiring the gate.
49+
// If the context expires, waitAndLockContext returns an error and does not acquire the gate.
50+
func (g *gate) waitAndLockContext(ctx context.Context) error {
51+
select {
52+
case <-g.set:
53+
return nil
54+
default:
55+
}
56+
select {
57+
case <-g.set:
58+
return nil
59+
case <-ctx.Done():
60+
return ctx.Err()
61+
}
62+
}
63+
64+
// waitWithLock releases an acquired gate until the condition is set.
65+
// The caller must have previously acquired the gate.
66+
// Upon return from waitWithLock, the gate will still be held.
67+
// If waitWithLock returns nil, the condition is set.
68+
func (g *gate) waitWithLock(ctx context.Context) error {
69+
g.unlock(false)
70+
err := g.waitAndLockContext(ctx)
71+
if err != nil {
72+
if g.lock() {
73+
// The condition was set in between the context expiring
74+
// and us reacquiring the gate.
75+
err = nil
76+
}
77+
}
78+
return err
79+
}
80+
81+
// lockIfSet acquires the gate if and only if the condition is set.
82+
func (g *gate) lockIfSet() (acquired bool) {
83+
select {
84+
case <-g.set:
85+
return true
86+
default:
87+
return false
88+
}
89+
}
90+
91+
// unlock sets the condition and releases the gate.
92+
func (g *gate) unlock(set bool) {
93+
if set {
94+
g.set <- struct{}{}
95+
} else {
96+
g.unset <- struct{}{}
97+
}
98+
}
99+
100+
// unlock sets the condition to the result of f and releases the gate.
101+
// Useful in defers.
102+
func (g *gate) unlockFunc(f func() bool) {
103+
g.unlock(f())
104+
}

internal/quic/gate_test.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.21
6+
7+
package quic
8+
9+
import (
10+
"context"
11+
"testing"
12+
"time"
13+
)
14+
15+
func TestGateLockAndUnlock(t *testing.T) {
16+
g := newGate()
17+
if set := g.lock(); set {
18+
t.Errorf("g.lock() of never-locked gate: true, want false")
19+
}
20+
unlockedc := make(chan struct{})
21+
donec := make(chan struct{})
22+
go func() {
23+
defer close(donec)
24+
set := g.lock()
25+
select {
26+
case <-unlockedc:
27+
default:
28+
t.Errorf("g.lock() succeeded while gate was held")
29+
}
30+
if !set {
31+
t.Errorf("g.lock() of set gate: false, want true")
32+
}
33+
g.unlock(false)
34+
}()
35+
time.Sleep(1 * time.Millisecond)
36+
close(unlockedc)
37+
g.unlock(true)
38+
<-donec
39+
if set := g.lock(); set {
40+
t.Errorf("g.lock() of unset gate: true, want false")
41+
}
42+
}
43+
44+
func TestGateWaitAndLock(t *testing.T) {
45+
g := newGate()
46+
set := false
47+
go func() {
48+
for i := 0; i < 3; i++ {
49+
g.lock()
50+
g.unlock(false)
51+
time.Sleep(1 * time.Millisecond)
52+
}
53+
g.lock()
54+
set = true
55+
g.unlock(true)
56+
}()
57+
g.waitAndLock()
58+
if !set {
59+
t.Errorf("g.waitAndLock() returned before gate was set")
60+
}
61+
}
62+
63+
func TestGateWaitAndLockContext(t *testing.T) {
64+
g := newGate()
65+
// waitAndLockContext is canceled
66+
ctx, cancel := context.WithCancel(context.Background())
67+
go func() {
68+
time.Sleep(1 * time.Millisecond)
69+
cancel()
70+
}()
71+
if err := g.waitAndLockContext(ctx); err != context.Canceled {
72+
t.Errorf("g.waitAndLockContext() = %v, want context.Canceled", err)
73+
}
74+
// waitAndLockContext succeeds
75+
set := false
76+
go func() {
77+
time.Sleep(1 * time.Millisecond)
78+
g.lock()
79+
set = true
80+
g.unlock(true)
81+
}()
82+
if err := g.waitAndLockContext(context.Background()); err != nil {
83+
t.Errorf("g.waitAndLockContext() = %v, want nil", err)
84+
}
85+
if !set {
86+
t.Errorf("g.waitAndLockContext() returned before gate was set")
87+
}
88+
g.unlock(true)
89+
// waitAndLockContext succeeds when the gate is set and the context is canceled
90+
if err := g.waitAndLockContext(ctx); err != nil {
91+
t.Errorf("g.waitAndLockContext() = %v, want nil", err)
92+
}
93+
}
94+
95+
func TestGateWaitWithLock(t *testing.T) {
96+
g := newGate()
97+
// waitWithLock is canceled
98+
ctx, cancel := context.WithCancel(context.Background())
99+
go func() {
100+
time.Sleep(1 * time.Millisecond)
101+
cancel()
102+
}()
103+
g.lock()
104+
if err := g.waitWithLock(ctx); err != context.Canceled {
105+
t.Errorf("g.waitWithLock() = %v, want context.Canceled", err)
106+
}
107+
// waitWithLock succeeds
108+
set := false
109+
go func() {
110+
g.lock()
111+
set = true
112+
g.unlock(true)
113+
}()
114+
time.Sleep(1 * time.Millisecond)
115+
if err := g.waitWithLock(context.Background()); err != nil {
116+
t.Errorf("g.waitWithLock() = %v, want nil", err)
117+
}
118+
if !set {
119+
t.Errorf("g.waitWithLock() returned before gate was set")
120+
}
121+
}
122+
123+
func TestGateLockIfSet(t *testing.T) {
124+
g := newGate()
125+
if locked := g.lockIfSet(); locked {
126+
t.Errorf("g.lockIfSet() of unset gate = %v, want false", locked)
127+
}
128+
g.lock()
129+
g.unlock(true)
130+
if locked := g.lockIfSet(); !locked {
131+
t.Errorf("g.lockIfSet() of set gate = %v, want true", locked)
132+
}
133+
}
134+
135+
func TestGateUnlockFunc(t *testing.T) {
136+
g := newGate()
137+
go func() {
138+
g.lock()
139+
defer g.unlockFunc(func() bool { return true })
140+
}()
141+
g.waitAndLock()
142+
}

internal/quic/queue.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.21
6+
7+
package quic
8+
9+
import "context"
10+
11+
// A queue is an unbounded queue of some item (new connections and streams).
12+
type queue[T any] struct {
13+
// The gate condition is set if the queue is non-empty or closed.
14+
gate gate
15+
err error
16+
q []T
17+
}
18+
19+
func newQueue[T any]() queue[T] {
20+
return queue[T]{gate: newGate()}
21+
}
22+
23+
// close closes the queue, causing pending and future pop operations
24+
// to return immediately with err.
25+
func (q *queue[T]) close(err error) {
26+
q.gate.lock()
27+
defer q.unlock()
28+
if q.err == nil {
29+
q.err = err
30+
}
31+
}
32+
33+
// put appends an item to the queue.
34+
// It returns true if the item was added, false if the queue is closed.
35+
func (q *queue[T]) put(v T) bool {
36+
q.gate.lock()
37+
defer q.unlock()
38+
if q.err != nil {
39+
return false
40+
}
41+
q.q = append(q.q, v)
42+
return true
43+
}
44+
45+
// get removes the first item from the queue, blocking until ctx is done, an item is available,
46+
// or the queue is closed.
47+
func (q *queue[T]) get(ctx context.Context) (T, error) {
48+
var zero T
49+
if err := q.gate.waitAndLockContext(ctx); err != nil {
50+
return zero, err
51+
}
52+
defer q.unlock()
53+
if q.err != nil {
54+
return zero, q.err
55+
}
56+
v := q.q[0]
57+
copy(q.q[:], q.q[1:])
58+
q.q[len(q.q)-1] = zero
59+
q.q = q.q[:len(q.q)-1]
60+
return v, nil
61+
}
62+
63+
func (q *queue[T]) unlock() {
64+
q.gate.unlock(q.err != nil || len(q.q) > 0)
65+
}

internal/quic/queue_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.21
6+
7+
package quic
8+
9+
import (
10+
"context"
11+
"io"
12+
"testing"
13+
"time"
14+
)
15+
16+
func TestQueue(t *testing.T) {
17+
nonblocking, cancel := context.WithCancel(context.Background())
18+
cancel()
19+
20+
q := newQueue[int]()
21+
if got, err := q.get(nonblocking); err != context.Canceled {
22+
t.Fatalf("q.get() = %v, %v, want nil, contex.Canceled", got, err)
23+
}
24+
25+
if !q.put(1) {
26+
t.Fatalf("q.put(1) = false, want true")
27+
}
28+
if !q.put(2) {
29+
t.Fatalf("q.put(2) = false, want true")
30+
}
31+
if got, err := q.get(nonblocking); got != 1 || err != nil {
32+
t.Fatalf("q.get() = %v, %v, want 1, nil", got, err)
33+
}
34+
if got, err := q.get(nonblocking); got != 2 || err != nil {
35+
t.Fatalf("q.get() = %v, %v, want 2, nil", got, err)
36+
}
37+
if got, err := q.get(nonblocking); err != context.Canceled {
38+
t.Fatalf("q.get() = %v, %v, want nil, contex.Canceled", got, err)
39+
}
40+
41+
go func() {
42+
time.Sleep(1 * time.Millisecond)
43+
q.put(3)
44+
}()
45+
if got, err := q.get(context.Background()); got != 3 || err != nil {
46+
t.Fatalf("q.get() = %v, %v, want 3, nil", got, err)
47+
}
48+
49+
if !q.put(4) {
50+
t.Fatalf("q.put(2) = false, want true")
51+
}
52+
q.close(io.EOF)
53+
if got, err := q.get(context.Background()); got != 0 || err != io.EOF {
54+
t.Fatalf("q.get() = %v, %v, want 0, io.EOF", got, err)
55+
}
56+
if q.put(5) {
57+
t.Fatalf("q.put(5) = true, want false")
58+
}
59+
}

0 commit comments

Comments
 (0)