Skip to content

Commit ed9b97c

Browse files
niaowdeadprogram
authored andcommitted
runtime: add cheap atomic condition variable
1 parent b40f250 commit ed9b97c

File tree

3 files changed

+172
-0
lines changed

3 files changed

+172
-0
lines changed

src/runtime/cond.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// +build !scheduler.none
2+
3+
package runtime
4+
5+
import (
6+
"internal/task"
7+
"sync/atomic"
8+
"unsafe"
9+
)
10+
11+
// notifiedPlaceholder is a placeholder task which is used to indicate that the condition variable has been notified.
12+
var notifiedPlaceholder task.Task
13+
14+
// Cond is a simplified condition variable, useful for notifying goroutines of interrupts.
15+
type Cond struct {
16+
t *task.Task
17+
}
18+
19+
// Notify sends a notification.
20+
// If the condition variable already has a pending notification, this returns false.
21+
func (c *Cond) Notify() bool {
22+
for {
23+
t := (*task.Task)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t))))
24+
switch t {
25+
case nil:
26+
// Nothing is waiting yet.
27+
// Apply the notification placeholder.
28+
if atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t)), unsafe.Pointer(t), unsafe.Pointer(&notifiedPlaceholder)) {
29+
return true
30+
}
31+
case &notifiedPlaceholder:
32+
// The condition variable has already been notified.
33+
return false
34+
default:
35+
// Unblock the waiting task.
36+
if atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t)), unsafe.Pointer(t), nil) {
37+
runqueuePushBack(t)
38+
return true
39+
}
40+
}
41+
}
42+
}
43+
44+
// Poll checks for a notification.
45+
// If a notification is found, it is cleared and this returns true.
46+
func (c *Cond) Poll() bool {
47+
for {
48+
t := (*task.Task)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t))))
49+
switch t {
50+
case nil:
51+
// No notifications are present.
52+
return false
53+
case &notifiedPlaceholder:
54+
// A notification arrived and there is no waiting goroutine.
55+
// Clear the notification and return.
56+
if atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t)), unsafe.Pointer(t), nil) {
57+
return true
58+
}
59+
default:
60+
// A task is blocked on the condition variable, which means it has not been notified.
61+
return false
62+
}
63+
}
64+
}
65+
66+
// Wait for a notification.
67+
// If the condition variable was previously notified, this returns immediately.
68+
func (c *Cond) Wait() {
69+
cur := task.Current()
70+
for {
71+
t := (*task.Task)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t))))
72+
switch t {
73+
case nil:
74+
// Condition variable has not been notified.
75+
// Block the current task on the condition variable.
76+
if atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t)), nil, unsafe.Pointer(cur)) {
77+
task.Pause()
78+
return
79+
}
80+
case &notifiedPlaceholder:
81+
// A notification arrived and there is no waiting goroutine.
82+
// Clear the notification and return.
83+
if atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t)), unsafe.Pointer(t), nil) {
84+
return
85+
}
86+
default:
87+
panic("interrupt.Cond: condition variable in use by another goroutine")
88+
}
89+
}
90+
}

src/runtime/cond_nosched.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// +build scheduler.none
2+
3+
package runtime
4+
5+
import "runtime/interrupt"
6+
7+
// Cond is a simplified condition variable, useful for notifying goroutines of interrupts.
8+
type Cond struct {
9+
notified bool
10+
}
11+
12+
// Notify sends a notification.
13+
// If the condition variable already has a pending notification, this returns false.
14+
func (c *Cond) Notify() bool {
15+
i := interrupt.Disable()
16+
prev := c.notified
17+
c.notified = true
18+
interrupt.Restore(i)
19+
return !prev
20+
}
21+
22+
// Poll checks for a notification.
23+
// If a notification is found, it is cleared and this returns true.
24+
func (c *Cond) Poll() bool {
25+
i := interrupt.Disable()
26+
notified := c.notified
27+
c.notified = false
28+
interrupt.Restore(i)
29+
return notified
30+
}
31+
32+
// Wait for a notification.
33+
// If the condition variable was previously notified, this returns immediately.
34+
func (c *Cond) Wait() {
35+
for !c.Poll() {
36+
waitForEvents()
37+
}
38+
}

testdata/coroutines.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"runtime"
45
"sync"
56
"time"
67
)
@@ -71,6 +72,8 @@ func main() {
7172
startSimpleFunc(emptyFunc)
7273

7374
time.Sleep(2 * time.Millisecond)
75+
76+
testCond()
7477
}
7578

7679
func acquire(m *sync.Mutex) {
@@ -127,3 +130,44 @@ type simpleFunc func()
127130

128131
func emptyFunc() {
129132
}
133+
134+
func testCond() {
135+
var cond runtime.Cond
136+
go func() {
137+
// Wait for the caller to wait on the cond.
138+
time.Sleep(time.Millisecond)
139+
140+
// Notify the caller.
141+
ok := cond.Notify()
142+
if !ok {
143+
panic("notification not sent")
144+
}
145+
146+
// This notification will be buffered inside the cond.
147+
ok = cond.Notify()
148+
if !ok {
149+
panic("notification not queued")
150+
}
151+
152+
// This notification should fail, since there is already one buffered.
153+
ok = cond.Notify()
154+
if ok {
155+
panic("notification double-sent")
156+
}
157+
}()
158+
159+
// Verify that the cond has no pending notifications.
160+
ok := cond.Poll()
161+
if ok {
162+
panic("unexpected early notification")
163+
}
164+
165+
// Wait for the goroutine spawned earlier to send a notification.
166+
cond.Wait()
167+
168+
// The goroutine should have also queued a notification in the cond.
169+
ok = cond.Poll()
170+
if !ok {
171+
panic("missing queued notification")
172+
}
173+
}

0 commit comments

Comments
 (0)