mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-08 04:53:42 +00:00
refactor coroutine lowering and tasks
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package task
|
||||
|
||||
const asserts = false
|
||||
|
||||
// Queue is a FIFO container of tasks.
|
||||
// The zero value is an empty queue.
|
||||
type Queue struct {
|
||||
head, tail *Task
|
||||
}
|
||||
|
||||
// Push a task onto the queue.
|
||||
func (q *Queue) Push(t *Task) {
|
||||
if asserts && t.Next != nil {
|
||||
panic("runtime: pushing a task to a queue with a non-nil Next pointer")
|
||||
}
|
||||
if q.tail != nil {
|
||||
q.tail.Next = t
|
||||
}
|
||||
q.tail = t
|
||||
t.Next = nil
|
||||
if q.head == nil {
|
||||
q.head = t
|
||||
}
|
||||
}
|
||||
|
||||
// Pop a task off of the queue.
|
||||
func (q *Queue) Pop() *Task {
|
||||
t := q.head
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
q.head = t.Next
|
||||
if q.tail == t {
|
||||
q.tail = nil
|
||||
}
|
||||
t.Next = nil
|
||||
return t
|
||||
}
|
||||
|
||||
// Append pops the contents of another queue and pushes them onto the end of this queue.
|
||||
func (q *Queue) Append(other *Queue) {
|
||||
if q.head == nil {
|
||||
q.head = other.head
|
||||
} else {
|
||||
q.tail.Next = other.head
|
||||
}
|
||||
q.tail = other.tail
|
||||
other.head, other.tail = nil, nil
|
||||
}
|
||||
|
||||
// Stack is a LIFO container of tasks.
|
||||
// The zero value is an empty stack.
|
||||
// This is slightly cheaper than a queue, so it can be preferable when strict ordering is not necessary.
|
||||
type Stack struct {
|
||||
top *Task
|
||||
}
|
||||
|
||||
// Push a task onto the stack.
|
||||
func (s *Stack) Push(t *Task) {
|
||||
if asserts && t.Next != nil {
|
||||
panic("runtime: pushing a task to a stack with a non-nil Next pointer")
|
||||
}
|
||||
s.top, t.Next = t, s.top
|
||||
}
|
||||
|
||||
// Pop a task off of the stack.
|
||||
func (s *Stack) Pop() *Task {
|
||||
t := s.top
|
||||
if t != nil {
|
||||
s.top = t.Next
|
||||
}
|
||||
t.Next = nil
|
||||
return t
|
||||
}
|
||||
|
||||
// tail follows the chain of tasks.
|
||||
// If t is nil, returns nil.
|
||||
// Otherwise, returns the task in the chain where the Next field is nil.
|
||||
func (t *Task) tail() *Task {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
for t.Next != nil {
|
||||
t = t.Next
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// Queue moves the contents of the stack into a queue.
|
||||
// Elements can be popped from the queue in the same order that they would be popped from the stack.
|
||||
func (s *Stack) Queue() Queue {
|
||||
head := s.top
|
||||
s.top = nil
|
||||
return Queue{
|
||||
head: head,
|
||||
tail: head.tail(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Task is a state of goroutine for scheduling purposes.
|
||||
type Task struct {
|
||||
// Next is a field which can be used to make a linked list of tasks.
|
||||
Next *Task
|
||||
|
||||
// Ptr is a field which can be used for storing a pointer.
|
||||
Ptr unsafe.Pointer
|
||||
|
||||
// Data is a field which can be used for storing state information.
|
||||
Data uint
|
||||
|
||||
// state is the underlying running state of the task.
|
||||
state state
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// +build scheduler.coroutines
|
||||
|
||||
package task
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// rawState is an underlying coroutine state exposed by llvm.coro.
|
||||
// This matches *i8 in LLVM.
|
||||
type rawState uint8
|
||||
|
||||
//go:export llvm.coro.resume
|
||||
func (s *rawState) resume()
|
||||
|
||||
type state struct{ *rawState }
|
||||
|
||||
//go:export llvm.coro.noop
|
||||
func noopState() *rawState
|
||||
|
||||
// Resume the task until it pauses or completes.
|
||||
func (t *Task) Resume() {
|
||||
t.state.resume()
|
||||
}
|
||||
|
||||
// setState is used by the compiler to set the state of the function at the beginning of a function call.
|
||||
// Returns the state of the caller.
|
||||
func (t *Task) setState(s *rawState) *rawState {
|
||||
caller := t.state
|
||||
t.state = state{s}
|
||||
return caller.rawState
|
||||
}
|
||||
|
||||
// returnTo is used by the compiler to return to the state of the caller.
|
||||
func (t *Task) returnTo(parent *rawState) {
|
||||
t.state = state{parent}
|
||||
t.returnCurrent()
|
||||
}
|
||||
|
||||
// returnCurrent is used by the compiler to return to the state of the caller in a case where the state is not replaced.
|
||||
func (t *Task) returnCurrent() {
|
||||
scheduleTask(t)
|
||||
}
|
||||
|
||||
//go:linkname scheduleTask runtime.runqueuePushBack
|
||||
func scheduleTask(*Task)
|
||||
|
||||
// setReturnPtr is used by the compiler to store the return buffer into the task.
|
||||
// This buffer is where the return value of a function that is about to be called will be stored.
|
||||
func (t *Task) setReturnPtr(buf unsafe.Pointer) {
|
||||
t.Ptr = buf
|
||||
}
|
||||
|
||||
// getReturnPtr is used by the compiler to get the return buffer stored into the task.
|
||||
// This is called at the beginning of an async function, and the return is stored into this buffer immediately before resuming the caller.
|
||||
func (t *Task) getReturnPtr() unsafe.Pointer {
|
||||
return t.Ptr
|
||||
}
|
||||
|
||||
// createTask returns a new task struct initialized with a no-op state.
|
||||
func createTask() *Task {
|
||||
return &Task{
|
||||
state: state{noopState()},
|
||||
}
|
||||
}
|
||||
|
||||
// start invokes a function in a new goroutine. Calls to this are inserted by the compiler.
|
||||
// The created goroutine starts running immediately.
|
||||
// This is implemented inside the compiler.
|
||||
func start(fn uintptr, args unsafe.Pointer)
|
||||
|
||||
// Current returns the current active task.
|
||||
// This is implemented inside the compiler.
|
||||
func Current() *Task
|
||||
|
||||
// Pause suspends the current running task.
|
||||
// This is implemented inside the compiler.
|
||||
func Pause()
|
||||
|
||||
type taskHolder interface {
|
||||
setState(*rawState) *rawState
|
||||
returnTo(*rawState)
|
||||
returnCurrent()
|
||||
setReturnPtr(unsafe.Pointer)
|
||||
getReturnPtr() unsafe.Pointer
|
||||
}
|
||||
|
||||
// If there are no direct references to the task methods, they will not be discovered by the compiler, and this will trigger a compiler error.
|
||||
// Instantiating this interface forces discovery of these methods.
|
||||
var _ = taskHolder((*Task)(nil))
|
||||
|
||||
func fake() {
|
||||
// Hack to ensure intrinsics are discovered.
|
||||
Current()
|
||||
go func() {}()
|
||||
Pause()
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// +build scheduler.none
|
||||
|
||||
package task
|
||||
|
||||
import "unsafe"
|
||||
|
||||
//go:linkname runtimePanic runtime.runtimePanic
|
||||
func runtimePanic(str string)
|
||||
|
||||
func Pause() {
|
||||
runtimePanic("scheduler is disabled")
|
||||
}
|
||||
|
||||
func Current() *Task {
|
||||
runtimePanic("scheduler is disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func start(fn uintptr, args unsafe.Pointer) {
|
||||
// The compiler will error if this is reachable.
|
||||
runtimePanic("scheduler is disabled")
|
||||
}
|
||||
|
||||
type state struct{}
|
||||
|
||||
func (t *Task) Resume() {
|
||||
runtimePanic("scheduler is disabled")
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// +build scheduler.tasks
|
||||
|
||||
package task
|
||||
|
||||
import "unsafe"
|
||||
|
||||
//go:linkname runtimePanic runtime.runtimePanic
|
||||
func runtimePanic(str string)
|
||||
|
||||
// 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)))
|
||||
|
||||
// state is a structure which holds a reference to the state of the task.
|
||||
// When the task is suspended, the registers are stored onto the stack and the stack pointer is stored into sp.
|
||||
type state struct {
|
||||
// sp is the stack pointer of the saved state.
|
||||
// When the task is inactive, the saved registers are stored at the top of the stack.
|
||||
sp uintptr
|
||||
|
||||
// canaryPtr points to the top word of the stack (the lowest address).
|
||||
// This is used to detect stack overflows.
|
||||
// When initializing the goroutine, the stackCanary constant is stored there.
|
||||
// If the stack overflowed, the word will likely no longer equal stackCanary.
|
||||
canaryPtr *uintptr
|
||||
}
|
||||
|
||||
// currentTask is the current running task, or nil if currently in the scheduler.
|
||||
var currentTask *Task
|
||||
|
||||
// Current returns the current active task.
|
||||
func Current() *Task {
|
||||
return currentTask
|
||||
}
|
||||
|
||||
// Pause suspends the current task and returns to the scheduler.
|
||||
// This function may only be called when running on a goroutine stack, not when running on the system stack or in an interrupt.
|
||||
func Pause() {
|
||||
// Check whether the canary (the lowest address of the stack) is still
|
||||
// valid. If it is not, a stack overflow has occured.
|
||||
if *currentTask.state.canaryPtr != stackCanary {
|
||||
runtimePanic("goroutine stack overflow")
|
||||
}
|
||||
currentTask.state.pause()
|
||||
}
|
||||
|
||||
// Resume the task until it pauses or completes.
|
||||
// This may only be called from the scheduler.
|
||||
func (t *Task) Resume() {
|
||||
currentTask = t
|
||||
t.state.resume()
|
||||
currentTask = nil
|
||||
}
|
||||
|
||||
// initialize the state and prepare to call the specified function with the specified argument bundle.
|
||||
func (s *state) initialize(fn uintptr, args unsafe.Pointer) {
|
||||
// Create a stack.
|
||||
stack := make([]uintptr, stackSize/unsafe.Sizeof(uintptr(0)))
|
||||
|
||||
// Invoke architecture-specific initialization.
|
||||
s.archInit(stack, fn, args)
|
||||
}
|
||||
|
||||
//go:linkname runqueuePushBack runtime.runqueuePushBack
|
||||
func runqueuePushBack(*Task)
|
||||
|
||||
// start creates and starts a new goroutine with the given function and arguments.
|
||||
// The new goroutine is scheduled to run later.
|
||||
func start(fn uintptr, args unsafe.Pointer) {
|
||||
t := &Task{}
|
||||
t.state.initialize(fn, args)
|
||||
runqueuePushBack(t)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// +build scheduler.tasks, cortexm
|
||||
|
||||
package task
|
||||
|
||||
import "unsafe"
|
||||
|
||||
const stackSize = 1024
|
||||
|
||||
// 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
|
||||
|
||||
pc uintptr
|
||||
}
|
||||
|
||||
// registers gets a pointer to the registers stored at the top of the stack.
|
||||
func (s *state) registers() *calleeSavedRegs {
|
||||
return (*calleeSavedRegs)(unsafe.Pointer(s.sp))
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// archInit runs architecture-specific setup for the goroutine startup.
|
||||
func (s *state) archInit(stack []uintptr, fn uintptr, args unsafe.Pointer) {
|
||||
// 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.
|
||||
s.canaryPtr = &stack[0]
|
||||
*s.canaryPtr = stackCanary
|
||||
|
||||
// Store the initial sp for the startTask function (implemented in assembly).
|
||||
s.sp = uintptr(unsafe.Pointer(&stack[uintptr(len(stack))-(unsafe.Sizeof(calleeSavedRegs{})/unsafe.Sizeof(uintptr(0)))]))
|
||||
|
||||
// Initialize the registers.
|
||||
// These will be popped off of the stack on the first resume of the goroutine.
|
||||
r := s.registers()
|
||||
|
||||
// Start the function at tinygo_startTask (defined in src/runtime/scheduler_cortexm.S).
|
||||
// This assembly code calls a function (passed in r4) with a single argument (passed in r5).
|
||||
// After the function returns, it calls Pause().
|
||||
r.pc = uintptr(unsafe.Pointer(&startTask))
|
||||
|
||||
// Pass the function to call in r4.
|
||||
// This function is a compiler-generated wrapper which loads arguments out of a struct pointer.
|
||||
// See createGoroutineStartWrapper (defined in compiler/goroutine.go) for more information.
|
||||
r.r4 = fn
|
||||
|
||||
// Pass the pointer to the arguments struct in r5.
|
||||
r.r5 = uintptr(args)
|
||||
}
|
||||
|
||||
func (s *state) resume() {
|
||||
switchToTask(s.sp)
|
||||
}
|
||||
|
||||
//export tinygo_switchToTask
|
||||
func switchToTask(uintptr)
|
||||
|
||||
//export tinygo_switchToScheduler
|
||||
func switchToScheduler(*uintptr)
|
||||
|
||||
func (s *state) pause() {
|
||||
switchToScheduler(&s.sp)
|
||||
}
|
||||
|
||||
//export tinygo_pause
|
||||
func pause() {
|
||||
Pause()
|
||||
}
|
||||
+24
-24
@@ -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
@@ -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 {
|
||||
|
||||
@@ -40,7 +40,10 @@ func main() {
|
||||
initAll()
|
||||
|
||||
// Compiler-generated call to main.main().
|
||||
callMain()
|
||||
go callMain()
|
||||
|
||||
// Run the scheduler.
|
||||
scheduler()
|
||||
}
|
||||
|
||||
func preinit() {
|
||||
|
||||
@@ -17,7 +17,8 @@ type timeUnit int64
|
||||
func main() {
|
||||
preinit()
|
||||
initAll()
|
||||
callMain()
|
||||
go callMain()
|
||||
scheduler()
|
||||
abort()
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ type timeUnit int64
|
||||
func main() {
|
||||
preinit()
|
||||
initAll()
|
||||
callMain()
|
||||
go callMain()
|
||||
scheduler()
|
||||
abort()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -21,7 +21,8 @@ var timestamp timeUnit
|
||||
func main() {
|
||||
preinit()
|
||||
initAll()
|
||||
callMain()
|
||||
go callMain()
|
||||
scheduler()
|
||||
arm.SemihostingCall(arm.SemihostingReportException, arm.SemihostingApplicationExit)
|
||||
abort()
|
||||
}
|
||||
|
||||
@@ -52,7 +52,8 @@ func main() {
|
||||
preinit()
|
||||
initPeripherals()
|
||||
initAll()
|
||||
callMain()
|
||||
go callMain()
|
||||
scheduler()
|
||||
abort()
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ func main() {
|
||||
systemInit()
|
||||
preinit()
|
||||
initAll()
|
||||
callMain()
|
||||
go callMain()
|
||||
scheduler()
|
||||
abort()
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ type timeUnit int64
|
||||
func main() {
|
||||
preinit()
|
||||
initAll()
|
||||
callMain()
|
||||
go callMain()
|
||||
scheduler()
|
||||
abort()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user