tinygo/src/runtime/scheduler.go
Ayke van Laethem 46d5ea8cf6 compiler: support returning values from async functions
This is implemented as follows:

  * The parent coroutine allocates space for the return value in its
    frame and stores a pointer to this frame in the parent coroutine
    handle.
  * The child coroutine obtains the alloca from its parent using the
    parent coroutine handle. It then stores the result value there.
  * The parent value reads the data from the alloca on resumption.
2019-05-05 16:46:50 +02:00

258 строки
7 КиБ
Go

package runtime
// This file implements the Go scheduler using coroutines.
// A goroutine contains a whole stack. A coroutine is just a single function.
// How do we use coroutines for goroutines, then?
// * Every function that contains a blocking call (like sleep) is marked
// blocking, and all it's parents (callers) are marked blocking as well
// transitively until the root (main.main or a go statement).
// * A blocking function that calls a non-blocking function is called as
// usual.
// * A blocking function that calls a blocking function passes its own
// coroutine handle as a parameter to the subroutine. When the subroutine
// returns, it will re-insert the parent into the scheduler.
// Note that a goroutine is generally called a 'task' for brevity and because
// that's the more common term among RTOSes. But a goroutine and a task are
// basically the same thing. Although, the code often uses the word 'task' to
// refer to both a coroutine and a goroutine, as most of the scheduler doesn't
// care about the difference.
//
// For more background on coroutines in LLVM:
// https://llvm.org/docs/Coroutines.html
import (
"unsafe"
)
const schedulerDebug = false
// A coroutine instance, wrapped here to provide some type safety. The value
// must not be used directly, it is meant to be used as an opaque *i8 in LLVM.
type coroutine uint8
//go:export llvm.coro.resume
func (t *coroutine) resume()
//go:export llvm.coro.destroy
func (t *coroutine) destroy()
//go:export llvm.coro.done
func (t *coroutine) done() bool
//go:export llvm.coro.promise
func (t *coroutine) _promise(alignment int32, from bool) unsafe.Pointer
// Get the promise belonging to a task.
func (t *coroutine) promise() *taskState {
return (*taskState)(t._promise(int32(unsafe.Alignof(taskState{})), false))
}
func makeGoroutine(*uint8) *uint8
// State/promise of a task. Internally represented as:
//
// {i8* next, i1 commaOk, i32/i64 data}
type taskState struct {
next *coroutine
commaOk bool // 'comma-ok' flag for channel receive operation
data uint
}
// Queues used by the scheduler.
//
// TODO: runqueueFront can be removed by making the run queue a circular linked
// list. The runqueueBack will simply refer to the front in the 'next' pointer.
var (
runqueueFront *coroutine
runqueueBack *coroutine
sleepQueue *coroutine
sleepQueueBaseTime timeUnit
)
// Simple logging, for debugging.
func scheduleLog(msg string) {
if schedulerDebug {
println(msg)
}
}
// Simple logging with a task pointer, for debugging.
func scheduleLogTask(msg string, t *coroutine) {
if schedulerDebug {
println(msg, t)
}
}
// Set the task to sleep for a given time.
//
// This is a compiler intrinsic.
func sleepTask(caller *coroutine, duration int64) {
if schedulerDebug {
println(" set sleep:", caller, uint(duration/tickMicros))
}
promise := caller.promise()
promise.data = uint(duration / tickMicros) // TODO: longer durations
addSleepTask(caller)
}
// Add a non-queued task to the run queue.
//
// This is a compiler intrinsic, and is called from a callee to reactivate the
// caller.
func activateTask(task *coroutine) {
if task == nil {
return
}
scheduleLogTask(" set runnable:", task)
runqueuePushBack(task)
}
func setTaskData(task *coroutine, value unsafe.Pointer) {
task.promise().data = uint(uintptr(value))
}
func getTaskData(task *coroutine) unsafe.Pointer {
return unsafe.Pointer(uintptr(task.promise().data))
}
// Add this task to the end of the run queue. May also destroy the task if it's
// done.
func runqueuePushBack(t *coroutine) {
if t.done() {
scheduleLogTask(" destroy task:", t)
t.destroy()
return
}
if schedulerDebug {
if t.promise().next != nil {
panic("runtime: runqueuePushBack: expected next task to be nil")
}
}
if runqueueBack == nil { // empty runqueue
scheduleLogTask(" add to runqueue front:", t)
runqueueBack = t
runqueueFront = t
} else {
scheduleLogTask(" add to runqueue back:", t)
lastTaskPromise := runqueueBack.promise()
lastTaskPromise.next = t
runqueueBack = t
}
}
// Get a task from the front of the run queue. Returns nil if there is none.
func runqueuePopFront() *coroutine {
t := runqueueFront
if t == nil {
return nil
}
if schedulerDebug {
println(" runqueuePopFront:", t)
}
promise := t.promise()
runqueueFront = promise.next
if runqueueFront == nil {
// Runqueue is empty now.
runqueueBack = nil
}
promise.next = nil
return t
}
// Add this task to the sleep queue, assuming its state is set to sleeping.
func addSleepTask(t *coroutine) {
if schedulerDebug {
if t.promise().next != nil {
panic("runtime: addSleepTask: expected next task to be nil")
}
}
now := ticks()
if sleepQueue == nil {
scheduleLog(" -> sleep new queue")
// Create new linked list for the sleep queue.
sleepQueue = t
sleepQueueBaseTime = now
return
}
// Make sure promise.data is relative to the queue time base.
promise := t.promise()
// Insert at front of sleep queue.
if promise.data < sleepQueue.promise().data {
scheduleLog(" -> sleep at start")
sleepQueue.promise().data -= promise.data
promise.next = sleepQueue
sleepQueue = t
return
}
// Add to sleep queue (in the middle or at the end).
queueIndex := sleepQueue
for {
promise.data -= queueIndex.promise().data
if queueIndex.promise().next == nil || queueIndex.promise().data > promise.data {
if queueIndex.promise().next == nil {
scheduleLog(" -> sleep at end")
promise.next = nil
} else {
scheduleLog(" -> sleep in middle")
promise.next = queueIndex.promise().next
promise.next.promise().data -= promise.data
}
queueIndex.promise().next = t
break
}
queueIndex = queueIndex.promise().next
}
}
// Run the scheduler until all tasks have finished.
func scheduler() {
// Main scheduler loop.
for {
scheduleLog("\n schedule")
now := ticks()
// Add tasks that are done sleeping to the end of the runqueue so they
// will be executed soon.
if sleepQueue != nil && now-sleepQueueBaseTime >= timeUnit(sleepQueue.promise().data) {
t := sleepQueue
scheduleLogTask(" awake:", t)
promise := t.promise()
sleepQueueBaseTime += timeUnit(promise.data)
sleepQueue = promise.next
promise.next = nil
runqueuePushBack(t)
}
t := runqueuePopFront()
if t == nil {
if sleepQueue == nil {
// No more tasks to execute.
// It would be nice if we could detect deadlocks here, because
// there might still be functions waiting on each other in a
// deadlock.
scheduleLog(" no tasks left!")
return
}
timeLeft := timeUnit(sleepQueue.promise().data) - (now - sleepQueueBaseTime)
if schedulerDebug {
println(" sleeping...", sleepQueue, uint(timeLeft))
}
sleepTicks(timeUnit(timeLeft))
if asyncScheduler {
// The sleepTicks function above only sets a timeout at which
// point the scheduler will be called again. It does not really
// sleep.
break
}
continue
}
// Run the given task.
scheduleLog(" <- runqueuePopFront")
scheduleLogTask(" run:", t)
t.resume()
}
}