aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorNia Weiss <[email protected]>2020-06-15 18:57:13 -0400
committerRon Evans <[email protected]>2020-10-14 13:35:00 +0200
commited9b97cc0df7a281ac1f1309ef00257cbcbe8d38 (patch)
tree93f984475fbb17dc1fec890b6a7edae192d37da9
parentb40f250530769330cec4a2ae0a9ba6e0c6ce058a (diff)
downloadtinygo-ed9b97cc0df7a281ac1f1309ef00257cbcbe8d38.tar.gz
tinygo-ed9b97cc0df7a281ac1f1309ef00257cbcbe8d38.zip
runtime: add cheap atomic condition variable
-rw-r--r--src/runtime/cond.go90
-rw-r--r--src/runtime/cond_nosched.go38
-rw-r--r--testdata/coroutines.go44
3 files changed, 172 insertions, 0 deletions
diff --git a/src/runtime/cond.go b/src/runtime/cond.go
new file mode 100644
index 000000000..76d3a377b
--- /dev/null
+++ b/src/runtime/cond.go
@@ -0,0 +1,90 @@
+// +build !scheduler.none
+
+package runtime
+
+import (
+ "internal/task"
+ "sync/atomic"
+ "unsafe"
+)
+
+// notifiedPlaceholder is a placeholder task which is used to indicate that the condition variable has been notified.
+var notifiedPlaceholder task.Task
+
+// Cond is a simplified condition variable, useful for notifying goroutines of interrupts.
+type Cond struct {
+ t *task.Task
+}
+
+// Notify sends a notification.
+// If the condition variable already has a pending notification, this returns false.
+func (c *Cond) Notify() bool {
+ for {
+ t := (*task.Task)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t))))
+ switch t {
+ case nil:
+ // Nothing is waiting yet.
+ // Apply the notification placeholder.
+ if atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t)), unsafe.Pointer(t), unsafe.Pointer(&notifiedPlaceholder)) {
+ return true
+ }
+ case &notifiedPlaceholder:
+ // The condition variable has already been notified.
+ return false
+ default:
+ // Unblock the waiting task.
+ if atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t)), unsafe.Pointer(t), nil) {
+ runqueuePushBack(t)
+ return true
+ }
+ }
+ }
+}
+
+// Poll checks for a notification.
+// If a notification is found, it is cleared and this returns true.
+func (c *Cond) Poll() bool {
+ for {
+ t := (*task.Task)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t))))
+ switch t {
+ case nil:
+ // No notifications are present.
+ return false
+ case &notifiedPlaceholder:
+ // A notification arrived and there is no waiting goroutine.
+ // Clear the notification and return.
+ if atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t)), unsafe.Pointer(t), nil) {
+ return true
+ }
+ default:
+ // A task is blocked on the condition variable, which means it has not been notified.
+ return false
+ }
+ }
+}
+
+// Wait for a notification.
+// If the condition variable was previously notified, this returns immediately.
+func (c *Cond) Wait() {
+ cur := task.Current()
+ for {
+ t := (*task.Task)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t))))
+ switch t {
+ case nil:
+ // Condition variable has not been notified.
+ // Block the current task on the condition variable.
+ if atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t)), nil, unsafe.Pointer(cur)) {
+ task.Pause()
+ return
+ }
+ case &notifiedPlaceholder:
+ // A notification arrived and there is no waiting goroutine.
+ // Clear the notification and return.
+ if atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t)), unsafe.Pointer(t), nil) {
+ return
+ }
+ default:
+ panic("interrupt.Cond: condition variable in use by another goroutine")
+ }
+ }
+}
diff --git a/src/runtime/cond_nosched.go b/src/runtime/cond_nosched.go
new file mode 100644
index 000000000..585b76935
--- /dev/null
+++ b/src/runtime/cond_nosched.go
@@ -0,0 +1,38 @@
+// +build scheduler.none
+
+package runtime
+
+import "runtime/interrupt"
+
+// Cond is a simplified condition variable, useful for notifying goroutines of interrupts.
+type Cond struct {
+ notified bool
+}
+
+// Notify sends a notification.
+// If the condition variable already has a pending notification, this returns false.
+func (c *Cond) Notify() bool {
+ i := interrupt.Disable()
+ prev := c.notified
+ c.notified = true
+ interrupt.Restore(i)
+ return !prev
+}
+
+// Poll checks for a notification.
+// If a notification is found, it is cleared and this returns true.
+func (c *Cond) Poll() bool {
+ i := interrupt.Disable()
+ notified := c.notified
+ c.notified = false
+ interrupt.Restore(i)
+ return notified
+}
+
+// Wait for a notification.
+// If the condition variable was previously notified, this returns immediately.
+func (c *Cond) Wait() {
+ for !c.Poll() {
+ waitForEvents()
+ }
+}
diff --git a/testdata/coroutines.go b/testdata/coroutines.go
index bb8acdbc6..17b2b0e3a 100644
--- a/testdata/coroutines.go
+++ b/testdata/coroutines.go
@@ -1,6 +1,7 @@
package main
import (
+ "runtime"
"sync"
"time"
)
@@ -71,6 +72,8 @@ func main() {
startSimpleFunc(emptyFunc)
time.Sleep(2 * time.Millisecond)
+
+ testCond()
}
func acquire(m *sync.Mutex) {
@@ -127,3 +130,44 @@ type simpleFunc func()
func emptyFunc() {
}
+
+func testCond() {
+ var cond runtime.Cond
+ go func() {
+ // Wait for the caller to wait on the cond.
+ time.Sleep(time.Millisecond)
+
+ // Notify the caller.
+ ok := cond.Notify()
+ if !ok {
+ panic("notification not sent")
+ }
+
+ // This notification will be buffered inside the cond.
+ ok = cond.Notify()
+ if !ok {
+ panic("notification not queued")
+ }
+
+ // This notification should fail, since there is already one buffered.
+ ok = cond.Notify()
+ if ok {
+ panic("notification double-sent")
+ }
+ }()
+
+ // Verify that the cond has no pending notifications.
+ ok := cond.Poll()
+ if ok {
+ panic("unexpected early notification")
+ }
+
+ // Wait for the goroutine spawned earlier to send a notification.
+ cond.Wait()
+
+ // The goroutine should have also queued a notification in the cond.
+ ok = cond.Poll()
+ if !ok {
+ panic("missing queued notification")
+ }
+}