tinygo/src/runtime/scheduler_tasks.go
Ayke van Laethem e4fc3bb66a compiler,runtime: fix new task-based scheduler
A bug was introduced in the previous commit that led to miscompilations
in the time.Sleep function when the scheduler was disabled, because
time.Sleep (implemented in the runtime) tried to switch to the scheduler
stack.

This commit restores the binary size of most examples to what it was
before, but still reduces static RAM consumption (.bss) slightly. This
gives me some confidence that it does indeed fix the introduced bug.
2019-08-17 08:42:23 +02:00

131 строка
4,1 КиБ
Go

// +build scheduler.tasks
package runtime
import "unsafe"
const stackSize = 1024
// Stack canary, to detect a stack overflow. The number is a random number
// generated by random.org. The bit fiddling dance is necessary because
// otherwise Go wouldn't allow the cast to a smaller integer size.
const stackCanary = uintptr(uint64(0x670c1333b83bf575) & uint64(^uintptr(0)))
var (
schedulerState = task{canary: stackCanary}
currentTask *task // currently running goroutine, or nil
)
// This type points to the bottom of the goroutine stack and contains some state
// that must be kept with the task. The last field is a canary, which is
// necessary to make sure that no stack overflow occured when switching tasks.
type task struct {
// The order of fields in this structs must be kept in sync with assembly!
calleeSavedRegs
sp uintptr
pc uintptr
taskState
canary uintptr // used to detect stack overflows
}
// getCoroutine returns the currently executing goroutine. It is used as an
// intrinsic when compiling channel operations, but is not necessary with the
// task-based scheduler.
func getCoroutine() *task {
return currentTask
}
// state is a small helper that returns the task state, and is provided for
// compatibility with the coroutine implementation.
//go:inline
func (t *task) state() *taskState {
return &t.taskState
}
// resume is a small helper that resumes this task until this task switches back
// to the scheduler.
func (t *task) resume() {
currentTask = t
swapTask(&schedulerState, t)
currentTask = nil
}
// swapTask saves the current state to oldTask (which must contain the current
// task state) and switches to newTask. Note that this function usually does
// return, when another task (perhaps newTask) switches back to the current
// task.
//
// As an additional protection, before switching tasks, it checks whether this
// goroutine has overflowed the stack.
func swapTask(oldTask, newTask *task) {
if oldTask.canary != stackCanary {
runtimePanic("goroutine stack overflow")
}
swapTaskLower(oldTask, newTask)
}
//go:linkname swapTaskLower tinygo_swapTask
func swapTaskLower(oldTask, newTask *task)
// Goexit terminates the currently running goroutine. No other goroutines are affected.
//
// Unlike the main Go implementation, no deffered calls will be run.
//export runtime.Goexit
func Goexit() {
// Swap without rescheduling first, effectively exiting the goroutine.
swapTask(currentTask, &schedulerState)
}
// startTask is a small wrapper function that sets up the first (and only)
// argument to the new goroutine and makes sure it is exited when the goroutine
// finishes.
//go:extern tinygo_startTask
var startTask [0]uint8
// startGoroutine starts a new goroutine with the given function pointer and
// argument. It creates a new goroutine stack, prepares it for execution, and
// adds it to the runqueue.
func startGoroutine(fn, args uintptr) {
stack := alloc(stackSize)
t := (*task)(stack)
t.sp = uintptr(stack) + stackSize
t.pc = uintptr(unsafe.Pointer(&startTask))
t.prepareStartTask(fn, args)
t.canary = stackCanary
scheduleLogTask(" start goroutine:", t)
runqueuePushBack(t)
}
//go:linkname sleep time.Sleep
func sleep(d int64) {
sleepTicks(timeUnit(d / tickMicros))
}
// sleepCurrentTask suspends the current goroutine. This is a compiler
// intrinsic. It replaces calls to time.Sleep when a scheduler is in use.
func sleepCurrentTask(d int64) {
sleepTask(currentTask, d)
swapTask(currentTask, &schedulerState)
}
// 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{}
func deadlock() {
Goexit()
}
// reactivateParent reactivates the parent goroutine. It is a no-op for the task
// based scheduler.
func reactivateParent(t *task) {
// Nothing to do here, tasks don't stop automatically.
}
// chanYield exits the current goroutine. Used in the channel implementation, to
// suspend the current goroutine until it is reactivated by a channel operation
// of a different goroutine.
func chanYield() {
Goexit()
}