mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-07 04:23:41 +00:00
286 lines
7.3 KiB
Go
286 lines
7.3 KiB
Go
package runtime
|
|
|
|
// This file implements the TinyGo scheduler. This scheduler is a very simple
|
|
// cooperative round robin scheduler, with a runqueue that contains a linked
|
|
// list of goroutines (tasks) that should be run next, in order of when they
|
|
// were added to the queue (first-in, first-out). It also contains a sleep queue
|
|
// with sleeping goroutines in order of when they should be re-activated.
|
|
//
|
|
// The scheduler is used both for the coroutine based scheduler and for the task
|
|
// based scheduler (see compiler/goroutine-lowering.go for a description). In
|
|
// both cases, the 'task' type is used to represent one goroutine. In the case
|
|
// of the task based scheduler, it literally is the goroutine itself: a pointer
|
|
// to the bottom of the stack where some important fields are kept. In the case
|
|
// of the coroutine-based scheduler, it is the coroutine pointer (a *i8 in
|
|
// LLVM).
|
|
|
|
import "unsafe"
|
|
|
|
const schedulerDebug = false
|
|
|
|
// State of a task. Internally represented as:
|
|
//
|
|
// {i8* next, i8* ptr, i32/i64 data}
|
|
type taskState struct {
|
|
next *task
|
|
ptr unsafe.Pointer
|
|
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 *task
|
|
runqueueBack *task
|
|
sleepQueue *task
|
|
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 *task) {
|
|
if schedulerDebug {
|
|
println("---", msg, t)
|
|
}
|
|
}
|
|
|
|
// Simple logging with a channel and task pointer.
|
|
func scheduleLogChan(msg string, ch *channel, t *task) {
|
|
if schedulerDebug {
|
|
println("---", msg, ch, t)
|
|
}
|
|
}
|
|
|
|
// deadlock is called when a goroutine cannot proceed any more, but is in theory
|
|
// not exited (so deferred calls won't run). This can happen for example in code
|
|
// like this, that blocks forever:
|
|
//
|
|
// select{}
|
|
//go:noinline
|
|
func deadlock() {
|
|
// call yield without requesting a wakeup
|
|
yield()
|
|
panic("unreachable")
|
|
}
|
|
|
|
// Goexit terminates the currently running goroutine. No other goroutines are affected.
|
|
//
|
|
// Unlike the main Go implementation, no deffered calls will be run.
|
|
//go:inline
|
|
func Goexit() {
|
|
// its really just a deadlock
|
|
deadlock()
|
|
}
|
|
|
|
// unblock unblocks a task and returns the next value
|
|
func unblock(t *task) *task {
|
|
state := t.state()
|
|
next := state.next
|
|
state.next = nil
|
|
activateTask(t)
|
|
return next
|
|
}
|
|
|
|
// unblockChain unblocks the next task on the stack/queue, returning it
|
|
// also updates the chain, putting the next element into the chain pointer
|
|
// if the chain is used as a queue, tail is used as a pointer to the final insertion point
|
|
// if the chain is used as a stack, tail should be nil
|
|
func unblockChain(chain **task, tail ***task) *task {
|
|
t := *chain
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
*chain = unblock(t)
|
|
if tail != nil && *chain == nil {
|
|
*tail = chain
|
|
}
|
|
return t
|
|
}
|
|
|
|
// dropChain drops a task from the given stack or queue
|
|
// if the chain is used as a queue, tail is used as a pointer to the field containing a pointer to the next insertion point
|
|
// if the chain is used as a stack, tail should be nil
|
|
func dropChain(t *task, chain **task, tail ***task) {
|
|
for c := chain; *c != nil; c = &((*c).state().next) {
|
|
if *c == t {
|
|
next := (*c).state().next
|
|
if next == nil && tail != nil {
|
|
*tail = c
|
|
}
|
|
*c = next
|
|
return
|
|
}
|
|
}
|
|
panic("runtime: task not in chain")
|
|
}
|
|
|
|
// Pause the current task for a given time.
|
|
//go:linkname sleep time.Sleep
|
|
func sleep(duration int64) {
|
|
addSleepTask(getCoroutine(), duration)
|
|
yield()
|
|
}
|
|
|
|
func avrSleep(duration int64) {
|
|
sleepTicks(timeUnit(duration / tickMicros))
|
|
}
|
|
|
|
// 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(t *task) {
|
|
if t == nil {
|
|
return
|
|
}
|
|
scheduleLogTask(" set runnable:", t)
|
|
runqueuePushBack(t)
|
|
}
|
|
|
|
// getTaskStateData is a helper function to get the current .data field of the
|
|
// goroutine state.
|
|
//go:inline
|
|
func getTaskStateData(t *task) uint {
|
|
return t.state().data
|
|
}
|
|
|
|
// Add this task to the end of the run queue. May also destroy the task if it's
|
|
// done.
|
|
func runqueuePushBack(t *task) {
|
|
if schedulerDebug {
|
|
scheduleLogTask(" pushing back:", t)
|
|
if t.state().next != nil {
|
|
panic("runtime: runqueuePushBack: expected next task to be nil")
|
|
}
|
|
}
|
|
if runqueueBack == nil { // empty runqueue
|
|
runqueueBack = t
|
|
runqueueFront = t
|
|
} else {
|
|
lastTaskState := runqueueBack.state()
|
|
lastTaskState.next = t
|
|
runqueueBack = t
|
|
}
|
|
}
|
|
|
|
// Get a task from the front of the run queue. Returns nil if there is none.
|
|
func runqueuePopFront() *task {
|
|
t := runqueueFront
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
state := t.state()
|
|
runqueueFront = state.next
|
|
if runqueueFront == nil {
|
|
// Runqueue is empty now.
|
|
runqueueBack = nil
|
|
}
|
|
state.next = nil
|
|
return t
|
|
}
|
|
|
|
// Add this task to the sleep queue, assuming its state is set to sleeping.
|
|
func addSleepTask(t *task, duration int64) {
|
|
if schedulerDebug {
|
|
println(" set sleep:", t, uint(duration/tickMicros))
|
|
if t.state().next != nil {
|
|
panic("runtime: addSleepTask: expected next task to be nil")
|
|
}
|
|
}
|
|
t.state().data = uint(duration / tickMicros) // TODO: longer durations
|
|
now := ticks()
|
|
if sleepQueue == nil {
|
|
scheduleLog(" -> sleep new queue")
|
|
|
|
// set new base time
|
|
sleepQueueBaseTime = now
|
|
}
|
|
|
|
// Add to sleep queue.
|
|
q := &sleepQueue
|
|
for ; *q != nil; q = &((*q).state()).next {
|
|
if t.state().data < (*q).state().data {
|
|
// this will finish earlier than the next - insert here
|
|
break
|
|
} else {
|
|
// this will finish later - adjust delay
|
|
t.state().data -= (*q).state().data
|
|
}
|
|
}
|
|
if *q != nil {
|
|
// cut delay time between this sleep task and the next
|
|
(*q).state().data -= t.state().data
|
|
}
|
|
t.state().next = *q
|
|
*q = t
|
|
}
|
|
|
|
// Run the scheduler until all tasks have finished.
|
|
func scheduler() {
|
|
// Main scheduler loop.
|
|
var now timeUnit
|
|
for {
|
|
scheduleLog("")
|
|
scheduleLog(" schedule")
|
|
if sleepQueue != nil {
|
|
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.state().data) {
|
|
t := sleepQueue
|
|
scheduleLogTask(" awake:", t)
|
|
state := t.state()
|
|
sleepQueueBaseTime += timeUnit(state.data)
|
|
sleepQueue = state.next
|
|
state.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.state().data) - (now - sleepQueueBaseTime)
|
|
if schedulerDebug {
|
|
println(" sleeping...", sleepQueue, uint(timeLeft))
|
|
for t := sleepQueue; t != nil; t = t.state().next {
|
|
println(" task sleeping:", t, timeUnit(t.state().data))
|
|
}
|
|
}
|
|
sleepTicks(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.
|
|
scheduleLogTask(" run:", t)
|
|
t.resume()
|
|
}
|
|
}
|
|
|
|
func Gosched() {
|
|
runqueuePushBack(getCoroutine())
|
|
yield()
|
|
}
|