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 ( "internal/task" ) const schedulerDebug = false var schedulerDone bool // Queues used by the scheduler. var ( runqueue task.Queue sleepQueue *task.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.Task) { if schedulerDebug { println("---", msg, t) } } // Simple logging with a channel and task pointer. func scheduleLogChan(msg string, ch *channel, t *task.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 task.Pause() 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() } // Add this task to the end of the run queue. func runqueuePushBack(t *task.Task) { runqueue.Push(t) } // Add this task to the sleep queue, assuming its state is set to sleeping. func addSleepTask(t *task.Task, duration timeUnit) { if schedulerDebug { println(" set sleep:", t, duration) if t.Next != nil { panic("runtime: addSleepTask: expected next task to be nil") } } t.Data = uint(duration) // 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).Next { if t.Data < (*q).Data { // this will finish earlier than the next - insert here break } else { // this will finish later - adjust delay t.Data -= (*q).Data } } if *q != nil { // cut delay time between this sleep task and the next (*q).Data -= t.Data } t.Next = *q *q = t } // Run the scheduler until all tasks have finished. func scheduler() { // Main scheduler loop. var now timeUnit for !schedulerDone { 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.Data) { t := sleepQueue scheduleLogTask(" awake:", t) sleepQueueBaseTime += timeUnit(t.Data) sleepQueue = t.Next t.Next = nil runqueue.Push(t) } t := runqueue.Pop() if t == nil { if sleepQueue == nil { if asyncScheduler { return } waitForEvents() continue } timeLeft := timeUnit(sleepQueue.Data) - (now - sleepQueueBaseTime) if schedulerDebug { println(" sleeping...", sleepQueue, uint(timeLeft)) for t := sleepQueue; t != nil; t = t.Next { println(" task sleeping:", t, timeUnit(t.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() { runqueue.Push(task.Current()) task.Pause() }