diff options
author | Nia Weiss <[email protected]> | 2020-06-15 18:57:13 -0400 |
---|---|---|
committer | Ron Evans <[email protected]> | 2020-10-14 13:35:00 +0200 |
commit | ed9b97cc0df7a281ac1f1309ef00257cbcbe8d38 (patch) | |
tree | 93f984475fbb17dc1fec890b6a7edae192d37da9 | |
parent | b40f250530769330cec4a2ae0a9ba6e0c6ce058a (diff) | |
download | tinygo-ed9b97cc0df7a281ac1f1309ef00257cbcbe8d38.tar.gz tinygo-ed9b97cc0df7a281ac1f1309ef00257cbcbe8d38.zip |
runtime: add cheap atomic condition variable
-rw-r--r-- | src/runtime/cond.go | 90 | ||||
-rw-r--r-- | src/runtime/cond_nosched.go | 38 | ||||
-rw-r--r-- | testdata/coroutines.go | 44 |
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(¬ifiedPlaceholder)) { + return true + } + case ¬ifiedPlaceholder: + // 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 ¬ifiedPlaceholder: + // 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 ¬ifiedPlaceholder: + // 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") + } +} |