refactor coroutine lowering and tasks

This commit is contained in:
Jaden Weiss
2020-01-03 00:23:37 -05:00
committed by Ayke
parent 2521cacb51
commit 6a50f25a48
40 changed files with 1830 additions and 1515 deletions
+24 -24
View File
@@ -24,6 +24,7 @@ package runtime
// element of the receiving coroutine and setting the 'comma-ok' value to false.
import (
"internal/task"
"unsafe"
)
@@ -46,7 +47,7 @@ type channelBlockedList struct {
// If this channel operation is not part of a select, then the pointer field of the state holds the data buffer.
// If this channel operation is part of a select, then the pointer field of the state holds the recieve buffer.
// If this channel operation is a receive, then the data field should be set to zero when resuming due to channel closure.
t *task
t *task.Task
// s is a pointer to the channel select state corresponding to this operation.
// This will be nil if and only if this channel operation is not part of a select statement.
@@ -141,24 +142,24 @@ func (ch *channel) resumeRX(ok bool) unsafe.Pointer {
b, ch.blocked = ch.blocked, ch.blocked.next
// get destination pointer
dst := b.t.state().ptr
dst := b.t.Ptr
if !ok {
// the result value is zero
memzero(dst, ch.elementSize)
b.t.state().data = 0
b.t.Data = 0
}
if b.s != nil {
// tell the select op which case resumed
b.t.state().ptr = unsafe.Pointer(b.s)
b.t.Ptr = unsafe.Pointer(b.s)
// detach associated operations
b.detach()
}
// push task onto runqueue
runqueuePushBack(b.t)
runqueue.Push(b.t)
return dst
}
@@ -171,21 +172,21 @@ func (ch *channel) resumeTX() unsafe.Pointer {
b, ch.blocked = ch.blocked, ch.blocked.next
// get source pointer
src := b.t.state().ptr
src := b.t.Ptr
if b.s != nil {
// use state's source pointer
src = b.s.value
// tell the select op which case resumed
b.t.state().ptr = unsafe.Pointer(b.s)
b.t.Ptr = unsafe.Pointer(b.s)
// detach associated operations
b.detach()
}
// push task onto runqueue
runqueuePushBack(b.t)
runqueue.Push(b.t)
return src
}
@@ -424,17 +425,16 @@ func chanSend(ch *channel, value unsafe.Pointer) {
}
// wait for reciever
sender := getCoroutine()
sender := task.Current()
ch.state = chanStateSend
senderState := sender.state()
senderState.ptr = value
sender.Ptr = value
ch.blocked = &channelBlockedList{
next: ch.blocked,
t: sender,
}
chanDebug(ch)
yield()
senderState.ptr = nil
task.Pause()
sender.Ptr = nil
}
// chanRecv receives a single value over a channel.
@@ -454,18 +454,17 @@ func chanRecv(ch *channel, value unsafe.Pointer) bool {
}
// wait for a value
receiver := getCoroutine()
receiver := task.Current()
ch.state = chanStateRecv
receiverState := receiver.state()
receiverState.ptr, receiverState.data = value, 1
receiver.Ptr, receiver.Data = value, 1
ch.blocked = &channelBlockedList{
next: ch.blocked,
t: receiver,
}
chanDebug(ch)
yield()
ok := receiverState.data == 1
receiverState.ptr, receiverState.data = nil, 0
task.Pause()
ok := receiver.Data == 1
receiver.Ptr, receiver.Data = nil, 0
return ok
}
@@ -515,7 +514,7 @@ func chanSelect(recvbuf unsafe.Pointer, states []chanSelectState, ops []channelB
for i, v := range states {
ops[i] = channelBlockedList{
next: v.ch.blocked,
t: getCoroutine(),
t: task.Current(),
s: &states[i],
allSelectOps: ops,
}
@@ -547,14 +546,15 @@ func chanSelect(recvbuf unsafe.Pointer, states []chanSelectState, ops []channelB
}
// expose rx buffer
getCoroutine().state().ptr = recvbuf
getCoroutine().state().data = 1
t := task.Current()
t.Ptr = recvbuf
t.Data = 1
// wait for one case to fire
yield()
task.Pause()
// figure out which one fired and return the ok value
return (uintptr(getCoroutine().state().ptr) - uintptr(unsafe.Pointer(&states[0]))) / unsafe.Sizeof(chanSelectState{}), getCoroutine().state().data != 0
return (uintptr(t.Ptr) - uintptr(unsafe.Pointer(&states[0]))) / unsafe.Sizeof(chanSelectState{}), t.Data != 0
}
// tryChanSelect is like chanSelect, but it does a non-blocking select operation.
+2 -11
View File
@@ -10,17 +10,8 @@ const Compiler = "tinygo"
// package.
func initAll()
// A function call to this function is replaced with one of the following,
// depending on whether the scheduler is necessary:
//
// Without scheduler:
//
// main.main()
//
// With scheduler:
//
// main.main()
// scheduler()
// callMain is a placeholder for the program main function.
// All references to this are replaced with references to the program main function by the compiler.
func callMain()
func GOMAXPROCS(n int) int {
+4 -1
View File
@@ -40,7 +40,10 @@ func main() {
initAll()
// Compiler-generated call to main.main().
callMain()
go callMain()
// Run the scheduler.
scheduler()
}
func preinit() {
+2 -1
View File
@@ -17,7 +17,8 @@ type timeUnit int64
func main() {
preinit()
initAll()
callMain()
go callMain()
scheduler()
abort()
}
+2 -1
View File
@@ -16,7 +16,8 @@ type timeUnit int64
func main() {
preinit()
initAll()
callMain()
go callMain()
scheduler()
abort()
}
-23
View File
@@ -40,29 +40,6 @@ func preinit() {
}
}
// calleeSavedRegs is the list of registers that must be saved and restored when
// switching between tasks. Also see scheduler_cortexm.S that relies on the
// exact layout of this struct.
type calleeSavedRegs struct {
r4 uintptr
r5 uintptr
r6 uintptr
r7 uintptr
r8 uintptr
r9 uintptr
r10 uintptr
r11 uintptr
}
// prepareStartTask stores fn and args in some callee-saved registers that can
// then be used by the startTask function (implemented in assembly) to set up
// the initial stack pointer and initial argument with the pointer to the object
// with the goroutine start arguments.
func (r *calleeSavedRegs) prepareStartTask(fn, args uintptr) {
r.r4 = fn
r.r5 = args
}
func abort() {
// disable all interrupts
arm.DisableInterrupts()
+2 -1
View File
@@ -21,7 +21,8 @@ var timestamp timeUnit
func main() {
preinit()
initAll()
callMain()
go callMain()
scheduler()
arm.SemihostingCall(arm.SemihostingReportException, arm.SemihostingApplicationExit)
abort()
}
+2 -1
View File
@@ -52,7 +52,8 @@ func main() {
preinit()
initPeripherals()
initAll()
callMain()
go callMain()
scheduler()
abort()
}
+2 -1
View File
@@ -22,7 +22,8 @@ func main() {
systemInit()
preinit()
initAll()
callMain()
go callMain()
scheduler()
abort()
}
+2 -1
View File
@@ -8,6 +8,7 @@ type timeUnit int64
func main() {
preinit()
initAll()
callMain()
go callMain()
scheduler()
abort()
}
+4 -1
View File
@@ -52,7 +52,10 @@ func main() int {
initAll()
// Compiler-generated call to main.main().
callMain()
go callMain()
// Run scheduler.
scheduler()
// For libc compatibility.
return 0
+5 -2
View File
@@ -21,7 +21,8 @@ func fd_write(id uint32, iovs *wasiIOVec, iovs_len uint, nwritten *uint) (errno
//export _start
func _start() {
initAll()
callMain()
go callMain()
scheduler()
}
// Using global variables to avoid heap allocation.
@@ -50,7 +51,9 @@ func setEventHandler(fn func()) {
//go:export resume
func resume() {
handleEvent()
go func() {
handleEvent()
}()
}
//go:export go_scheduler
+31 -145
View File
@@ -14,27 +14,16 @@ package runtime
// of the coroutine-based scheduler, it is the coroutine pointer (a *i8 in
// LLVM).
import "unsafe"
import (
"internal/task"
)
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
runqueue task.Queue
sleepQueue *task.Task
sleepQueueBaseTime timeUnit
)
@@ -46,14 +35,14 @@ func scheduleLog(msg string) {
}
// Simple logging with a task pointer, for debugging.
func scheduleLogTask(msg string, t *task) {
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) {
func scheduleLogChan(msg string, ch *channel, t *task.Task) {
if schedulerDebug {
println("---", msg, ch, t)
}
@@ -67,7 +56,7 @@ func scheduleLogChan(msg string, ch *channel, t *task) {
//go:noinline
func deadlock() {
// call yield without requesting a wakeup
yield()
task.Pause()
panic("unreachable")
}
@@ -80,122 +69,20 @@ func Goexit() {
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 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, duration int64) {
func addSleepTask(t *task.Task, duration int64) {
if schedulerDebug {
println(" set sleep:", t, uint(duration/tickMicros))
if t.state().next != nil {
if t.Next != nil {
panic("runtime: addSleepTask: expected next task to be nil")
}
}
t.state().data = uint(duration / tickMicros) // TODO: longer durations
t.Data = uint(duration / tickMicros) // TODO: longer durations
now := ticks()
if sleepQueue == nil {
scheduleLog(" -> sleep new queue")
@@ -206,20 +93,20 @@ func addSleepTask(t *task, duration int64) {
// Add to sleep queue.
q := &sleepQueue
for ; *q != nil; q = &((*q).state()).next {
if t.state().data < (*q).state().data {
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.state().data -= (*q).state().data
t.Data -= (*q).Data
}
}
if *q != nil {
// cut delay time between this sleep task and the next
(*q).state().data -= t.state().data
(*q).Data -= t.Data
}
t.state().next = *q
t.Next = *q
*q = t
}
@@ -236,17 +123,16 @@ func scheduler() {
// 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) {
if sleepQueue != nil && now-sleepQueueBaseTime >= timeUnit(sleepQueue.Data) {
t := sleepQueue
scheduleLogTask(" awake:", t)
state := t.state()
sleepQueueBaseTime += timeUnit(state.data)
sleepQueue = state.next
state.next = nil
runqueuePushBack(t)
sleepQueueBaseTime += timeUnit(t.Data)
sleepQueue = t.Next
t.Next = nil
runqueue.Push(t)
}
t := runqueuePopFront()
t := runqueue.Pop()
if t == nil {
if sleepQueue == nil {
// No more tasks to execute.
@@ -256,11 +142,11 @@ func scheduler() {
scheduleLog(" no tasks left!")
return
}
timeLeft := timeUnit(sleepQueue.state().data) - (now - sleepQueueBaseTime)
timeLeft := timeUnit(sleepQueue.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))
for t := sleepQueue; t != nil; t = t.Next {
println(" task sleeping:", t, timeUnit(t.Data))
}
}
sleepTicks(timeLeft)
@@ -275,11 +161,11 @@ func scheduler() {
// Run the given task.
scheduleLogTask(" run:", t)
t.resume()
t.Resume()
}
}
func Gosched() {
runqueuePushBack(getCoroutine())
yield()
runqueue.Push(task.Current())
task.Pause()
}
+12
View File
@@ -0,0 +1,12 @@
// +build !scheduler.none
package runtime
import "internal/task"
// Pause the current task for a given time.
//go:linkname sleep time.Sleep
func sleep(duration int64) {
addSleepTask(task.Current(), duration)
task.Pause()
}
-95
View File
@@ -2,103 +2,8 @@
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 we use the type 'task' to refer to a coroutine, for compatibility
// with the task-based scheduler. A task type here does not represent the whole
// task, but just the topmost coroutine. For most of the scheduler, this
// difference doesn't matter.
//
// For more background on coroutines in LLVM:
// https://llvm.org/docs/Coroutines.html
import "unsafe"
// 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 task uint8
//go:export llvm.coro.resume
func (t *task) resume()
//go:export llvm.coro.destroy
func (t *task) destroy()
//go:export llvm.coro.done
func (t *task) done() bool
//go:export llvm.coro.promise
func (t *task) _promise(alignment int32, from bool) unsafe.Pointer
// Get the state belonging to a task.
func (t *task) state() *taskState {
return (*taskState)(t._promise(int32(unsafe.Alignof(taskState{})), false))
}
func makeGoroutine(uintptr) uintptr
// Compiler stub to get the current goroutine. Calls to this function are
// removed in the goroutine lowering pass.
func getCoroutine() *task
// setTaskStatePtr is a helper function to set the current .ptr field of a
// coroutine promise.
func setTaskStatePtr(t *task, value unsafe.Pointer) {
t.state().ptr = value
}
// getTaskStatePtr is a helper function to get the current .ptr field from a
// coroutine promise.
func getTaskStatePtr(t *task) unsafe.Pointer {
if t == nil {
blockingPanic()
}
return t.state().ptr
}
// yield suspends execution of the current goroutine
// any wakeups must be configured before calling yield
func yield()
// getSystemStackPointer returns the current stack pointer of the system stack.
// This is always the current stack pointer.
func getSystemStackPointer() uintptr {
return getCurrentStackPointer()
}
func fakeCoroutine(dst **task) {
*dst = getCoroutine()
for {
yield()
}
}
func getFakeCoroutine() *task {
// this isnt defined behavior, but this is what our implementation does
// this is really a horrible hack
var t *task
go fakeCoroutine(&t)
// the first line of fakeCoroutine will have completed by now
return t
}
// noret is a placeholder that can be used to indicate that an async function is not going to directly return here
func noret()
func getParentHandle() *task
func llvmCoroRefHolder() {
noret()
getParentHandle()
getCoroutine()
}
+4 -5
View File
@@ -17,7 +17,7 @@ tinygo_startTask:
blx r4
// After return, exit this goroutine. This is a tail call.
bl runtime.yield
bl tinygo_pause
.section .text.tinygo_getSystemStackPointer
.global tinygo_getSystemStackPointer
@@ -35,24 +35,23 @@ tinygo_getSystemStackPointer:
.global tinygo_switchToScheduler
.type tinygo_switchToScheduler, %function
tinygo_switchToScheduler:
// r0 = oldTask *task
// r0 = sp *uintptr
// Currently on the task stack (SP=PSP). We need to store the position on
// the stack where the in-use registers will be stored.
mov r1, sp
subs r1, #36
str r1, [r0, #36]
str r1, [r0]
b tinygo_swapTask
.global tinygo_switchToTask
.type tinygo_switchToTask, %function
tinygo_switchToTask:
// r0 = newTask *task
// r0 = sp uintptr
// Currently on the scheduler stack (SP=MSP). We'll have to update the PSP,
// and then we can invoke swapTask.
ldr r0, [r0, #36]
msr PSP, r0
// Continue executing in the swapTask function, which swaps the stack
+14
View File
@@ -0,0 +1,14 @@
// +build scheduler.none
package runtime
//go:linkname sleep time.Sleep
func sleep(duration int64) {
sleepTicks(timeUnit(duration / tickMicros))
}
// getSystemStackPointer returns the current stack pointer of the system stack.
// This is always the current stack pointer.
func getSystemStackPointer() uintptr {
return getCurrentStackPointer()
}
-104
View File
@@ -2,110 +2,6 @@
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 (
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
pc uintptr
sp uintptr
taskState
canaryPtr *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.
//go:inline
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
switchToTask(t)
currentTask = nil
}
// switchToScheduler saves the current state on the stack, saves the current
// stack pointer in the task, and switches to the scheduler. It must only be
// called when actually running on this task.
// When it returns, the scheduler has switched back to this task (for example,
// after a blocking operation completed).
//export tinygo_switchToScheduler
func switchToScheduler(t *task)
// switchToTask switches from the scheduler to the task. It must only be called
// from the scheduler.
// When this function returns, the task just yielded control back to the
// scheduler.
//export tinygo_switchToTask
func switchToTask(t *task)
// 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)(unsafe.Pointer(uintptr(stack) + stackSize - unsafe.Sizeof(task{})))
// Set up the stack canary, a random number that should be checked when
// switching from the task back to the scheduler. The stack canary pointer
// points to the first word of the stack. If it has changed between now and
// the next stack switch, there was a stack overflow.
t.canaryPtr = (*uintptr)(unsafe.Pointer(stack))
*t.canaryPtr = stackCanary
// Store the initial sp/pc for the startTask function (implemented in
// assembly).
t.sp = uintptr(stack) + stackSize - unsafe.Sizeof(task{})
t.pc = uintptr(unsafe.Pointer(&startTask))
t.prepareStartTask(fn, args)
scheduleLogTask(" start goroutine:", t)
runqueuePushBack(t)
}
// yield suspends execution of the current goroutine
// any wakeups must be configured before calling yield
//export runtime.yield
func yield() {
// Check whether the canary (the lowest address of the stack) is still
// valid. If it is not, a stack overflow has occured.
if *currentTask.canaryPtr != stackCanary {
runtimePanic("goroutine stack overflow")
}
switchToScheduler(currentTask)
}
// getSystemStackPointer returns the current stack pointer of the system stack.
// This is not necessarily the same as the current stack pointer.
//export tinygo_getSystemStackPointer