mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-05 03:27:48 +00:00
all: rewrite goroutine lowering
Before this commit, goroutine support was spread through the compiler. This commit changes this support, so that the compiler itself only generates simple intrinsics and leaves the real support to a compiler pass that runs as one of the TinyGo-specific optimization passes. The biggest change, that was done together with the rewrite, was support for goroutines in WebAssembly for JavaScript. The challenge in JavaScript is that in general no blocking operations are allowed, which means that programs that call time.Sleep() but do not start goroutines also have to be scheduled by the scheduler.
This commit is contained in:
@@ -10,8 +10,8 @@ const Compiler = "tgo"
|
||||
// package.
|
||||
func initAll()
|
||||
|
||||
// The compiler will insert the call to main.main() here, depending on whether
|
||||
// the scheduler is necessary.
|
||||
// A function call to this function is replaced withone of the following,
|
||||
// depending on whether the scheduler is necessary:
|
||||
//
|
||||
// Without scheduler:
|
||||
//
|
||||
@@ -19,9 +19,9 @@ func initAll()
|
||||
//
|
||||
// With scheduler:
|
||||
//
|
||||
// coroutine := main.main(nil)
|
||||
// scheduler(coroutine)
|
||||
func mainWrapper()
|
||||
// main.main()
|
||||
// scheduler()
|
||||
func callMain()
|
||||
|
||||
func GOMAXPROCS(n int) int {
|
||||
// Note: setting GOMAXPROCS is ignored.
|
||||
|
||||
@@ -15,7 +15,7 @@ type timeUnit int64
|
||||
func main() {
|
||||
preinit()
|
||||
initAll()
|
||||
mainWrapper()
|
||||
callMain()
|
||||
abort()
|
||||
}
|
||||
|
||||
@@ -238,6 +238,8 @@ type isrFlag bool
|
||||
|
||||
var timerWakeup isrFlag
|
||||
|
||||
const asyncScheduler = false
|
||||
|
||||
// sleepTicks should sleep for d number of microseconds.
|
||||
func sleepTicks(d timeUnit) {
|
||||
for d != 0 {
|
||||
|
||||
@@ -41,7 +41,7 @@ func main() {
|
||||
preinit()
|
||||
initAll()
|
||||
postinit()
|
||||
mainWrapper()
|
||||
callMain()
|
||||
abort()
|
||||
}
|
||||
|
||||
@@ -71,6 +71,8 @@ func putchar(c byte) {
|
||||
machine.UART0.WriteByte(c)
|
||||
}
|
||||
|
||||
const asyncScheduler = false
|
||||
|
||||
// Sleep this number of ticks of 16ms.
|
||||
//
|
||||
// TODO: not very accurate. Improve accuracy by calibrating on startup and every
|
||||
|
||||
@@ -20,7 +20,7 @@ func main() {
|
||||
systemInit()
|
||||
preinit()
|
||||
initAll()
|
||||
mainWrapper()
|
||||
callMain()
|
||||
abort()
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ func putchar(c byte) {
|
||||
machine.UART0.WriteByte(c)
|
||||
}
|
||||
|
||||
const asyncScheduler = false
|
||||
|
||||
func sleepTicks(d timeUnit) {
|
||||
for d != 0 {
|
||||
ticks() // update timestamp
|
||||
|
||||
@@ -20,11 +20,13 @@ var timestamp timeUnit
|
||||
func main() {
|
||||
preinit()
|
||||
initAll()
|
||||
mainWrapper()
|
||||
callMain()
|
||||
arm.SemihostingCall(arm.SemihostingReportException, arm.SemihostingApplicationExit)
|
||||
abort()
|
||||
}
|
||||
|
||||
const asyncScheduler = false
|
||||
|
||||
func sleepTicks(d timeUnit) {
|
||||
// TODO: actually sleep here for the given time.
|
||||
timestamp += d
|
||||
|
||||
@@ -8,6 +8,6 @@ type timeUnit int64
|
||||
func main() {
|
||||
preinit()
|
||||
initAll()
|
||||
mainWrapper()
|
||||
callMain()
|
||||
abort()
|
||||
}
|
||||
|
||||
@@ -107,6 +107,8 @@ func initTIM() {
|
||||
arm.EnableIRQ(stm32.IRQ_TIM3)
|
||||
}
|
||||
|
||||
const asyncScheduler = false
|
||||
|
||||
// sleepTicks should sleep for specific number of microseconds.
|
||||
func sleepTicks(d timeUnit) {
|
||||
for d != 0 {
|
||||
|
||||
@@ -46,8 +46,8 @@ func main() int {
|
||||
// Run initializers of all packages.
|
||||
initAll()
|
||||
|
||||
// Compiler-generated wrapper to main.main().
|
||||
mainWrapper()
|
||||
// Compiler-generated call to main.main().
|
||||
callMain()
|
||||
|
||||
// For libc compatibility.
|
||||
return 0
|
||||
@@ -57,6 +57,8 @@ func putchar(c byte) {
|
||||
_putchar(int(c))
|
||||
}
|
||||
|
||||
const asyncScheduler = false
|
||||
|
||||
func sleepTicks(d timeUnit) {
|
||||
usleep(uint(d) / 1000)
|
||||
}
|
||||
|
||||
+15
-11
@@ -6,11 +6,9 @@ import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type timeUnit int64
|
||||
type timeUnit float64 // time in milliseconds, just like Date.now() in JavaScript
|
||||
|
||||
const tickMicros = 1
|
||||
|
||||
var timestamp timeUnit
|
||||
const tickMicros = 1000000
|
||||
|
||||
//go:export io_get_stdout
|
||||
func io_get_stdout() int32
|
||||
@@ -32,21 +30,27 @@ func _start() {
|
||||
//go:export cwa_main
|
||||
func cwa_main() {
|
||||
initAll() // _start is not called by olin/cwa so has to be called here
|
||||
mainWrapper()
|
||||
callMain()
|
||||
}
|
||||
|
||||
func putchar(c byte) {
|
||||
resource_write(stdout, &c, 1)
|
||||
}
|
||||
|
||||
func sleepTicks(d timeUnit) {
|
||||
// TODO: actually sleep here for the given time.
|
||||
timestamp += d
|
||||
//go:export go_scheduler
|
||||
func go_scheduler() {
|
||||
scheduler()
|
||||
}
|
||||
|
||||
func ticks() timeUnit {
|
||||
return timestamp
|
||||
}
|
||||
const asyncScheduler = true
|
||||
|
||||
// This function is called by the scheduler.
|
||||
// Schedule a call to runtime.scheduler, do not actually sleep.
|
||||
//go:export runtime.sleepTicks
|
||||
func sleepTicks(d timeUnit)
|
||||
|
||||
//go:export runtime.ticks
|
||||
func ticks() timeUnit
|
||||
|
||||
// Abort executes the wasm 'unreachable' instruction.
|
||||
func abort() {
|
||||
|
||||
+29
-68
@@ -9,14 +9,13 @@ package runtime
|
||||
// * 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 and will make sure it's
|
||||
// own coroutine is removed from the scheduler. When the subroutine returns,
|
||||
// it will re-insert the parent into the scheduler.
|
||||
// coroutine handle as a parameter to the subroutine. When the subroutine
|
||||
// returns, it will re-insert the parent into the scheduler.
|
||||
// Note that a goroutine is generally called a 'task' for brevity and because
|
||||
// that's the more common term among RTOSes. But a goroutine and a task are
|
||||
// basically the same thing. Although, the code often uses the word 'task' to
|
||||
// refer to both a coroutine and a goroutine, as most of the scheduler isn't
|
||||
// aware of the difference.
|
||||
// refer to both a coroutine and a goroutine, as most of the scheduler doesn't
|
||||
// care about the difference.
|
||||
//
|
||||
// For more background on coroutines in LLVM:
|
||||
// https://llvm.org/docs/Coroutines.html
|
||||
@@ -45,25 +44,19 @@ func (t *coroutine) _promise(alignment int32, from bool) unsafe.Pointer
|
||||
|
||||
// Get the promise belonging to a task.
|
||||
func (t *coroutine) promise() *taskState {
|
||||
return (*taskState)(t._promise(4, false))
|
||||
return (*taskState)(t._promise(int32(unsafe.Alignof(taskState{})), false))
|
||||
}
|
||||
|
||||
func makeGoroutine(*uint8) *uint8
|
||||
|
||||
// State/promise of a task. Internally represented as:
|
||||
//
|
||||
// {i8 state, i32 data, i8* next}
|
||||
// {i8* next, i32/i64 data}
|
||||
type taskState struct {
|
||||
state uint8
|
||||
data uint32
|
||||
next *coroutine
|
||||
next *coroutine
|
||||
data uint
|
||||
}
|
||||
|
||||
// Various states a task can be in.
|
||||
const (
|
||||
TASK_STATE_RUNNABLE = iota
|
||||
TASK_STATE_SLEEP
|
||||
TASK_STATE_CALL // waiting for a sub-coroutine
|
||||
)
|
||||
|
||||
// Queues used by the scheduler.
|
||||
//
|
||||
// TODO: runqueueFront can be removed by making the run queue a circular linked
|
||||
@@ -89,48 +82,28 @@ func scheduleLogTask(msg string, t *coroutine) {
|
||||
}
|
||||
}
|
||||
|
||||
// Set the task state to sleep for a given time.
|
||||
// Set the task to sleep for a given time.
|
||||
//
|
||||
// This is a compiler intrinsic.
|
||||
func sleepTask(caller *coroutine, duration int64) {
|
||||
if schedulerDebug {
|
||||
println(" set state sleep:", caller, uint32(duration/tickMicros))
|
||||
println(" set sleep:", caller, uint(duration/tickMicros))
|
||||
}
|
||||
promise := caller.promise()
|
||||
promise.state = TASK_STATE_SLEEP
|
||||
promise.data = uint32(duration / tickMicros) // TODO: longer durations
|
||||
promise.data = uint(duration / tickMicros) // TODO: longer durations
|
||||
addSleepTask(caller)
|
||||
}
|
||||
|
||||
// Wait for the result of an async call. This means that the parent goroutine
|
||||
// will be removed from the runqueue and be rescheduled by the callee.
|
||||
// Add a non-queued task to the run queue.
|
||||
//
|
||||
// This is a compiler intrinsic.
|
||||
func waitForAsyncCall(caller *coroutine) {
|
||||
scheduleLogTask(" set state call:", caller)
|
||||
promise := caller.promise()
|
||||
promise.state = TASK_STATE_CALL
|
||||
}
|
||||
|
||||
// Add a task to the runnable or sleep queue, depending on the state.
|
||||
//
|
||||
// This is a compiler intrinsic.
|
||||
func yieldToScheduler(t *coroutine) {
|
||||
if t == nil {
|
||||
// This is a compiler intrinsic, and is called from a callee to reactivate the
|
||||
// caller.
|
||||
func activateTask(task *coroutine) {
|
||||
if task == nil {
|
||||
return
|
||||
}
|
||||
// See what we should do with this task: try to execute it directly
|
||||
// again or let it sleep for a bit.
|
||||
promise := t.promise()
|
||||
if promise.state == TASK_STATE_CALL {
|
||||
scheduleLogTask(" set waiting for call:", t)
|
||||
return // calling an async task, the subroutine will re-active the parent
|
||||
} else if promise.state == TASK_STATE_SLEEP && promise.data != 0 {
|
||||
scheduleLogTask(" set sleeping:", t)
|
||||
addSleepTask(t)
|
||||
} else {
|
||||
scheduleLogTask(" set runnable:", t)
|
||||
runqueuePushBack(t)
|
||||
}
|
||||
scheduleLogTask(" set runnable:", task)
|
||||
runqueuePushBack(task)
|
||||
}
|
||||
|
||||
// Add this task to the end of the run queue. May also destroy the task if it's
|
||||
@@ -145,9 +118,6 @@ func runqueuePushBack(t *coroutine) {
|
||||
if t.promise().next != nil {
|
||||
panic("runtime: runqueuePushBack: expected next task to be nil")
|
||||
}
|
||||
if t.promise().state != TASK_STATE_RUNNABLE {
|
||||
panic("runtime: runqueuePushBack: expected task state to be runnable")
|
||||
}
|
||||
}
|
||||
if runqueueBack == nil { // empty runqueue
|
||||
scheduleLogTask(" add to runqueue front:", t)
|
||||
@@ -169,10 +139,6 @@ func runqueuePopFront() *coroutine {
|
||||
}
|
||||
if schedulerDebug {
|
||||
println(" runqueuePopFront:", t)
|
||||
// Sanity checking.
|
||||
if t.promise().state != TASK_STATE_RUNNABLE {
|
||||
panic("runtime: runqueuePopFront: task not runnable")
|
||||
}
|
||||
}
|
||||
promise := t.promise()
|
||||
runqueueFront = promise.next
|
||||
@@ -190,9 +156,6 @@ func addSleepTask(t *coroutine) {
|
||||
if t.promise().next != nil {
|
||||
panic("runtime: addSleepTask: expected next task to be nil")
|
||||
}
|
||||
if t.promise().state != TASK_STATE_SLEEP {
|
||||
panic("runtime: addSleepTask: task not sleeping")
|
||||
}
|
||||
}
|
||||
now := ticks()
|
||||
if sleepQueue == nil {
|
||||
@@ -236,11 +199,7 @@ func addSleepTask(t *coroutine) {
|
||||
}
|
||||
|
||||
// Run the scheduler until all tasks have finished.
|
||||
// It takes an initial task (main.main) to bootstrap.
|
||||
func scheduler(main *coroutine) {
|
||||
// Initial task.
|
||||
yieldToScheduler(main)
|
||||
|
||||
func scheduler() {
|
||||
// Main scheduler loop.
|
||||
for {
|
||||
scheduleLog("\n schedule")
|
||||
@@ -254,7 +213,6 @@ func scheduler(main *coroutine) {
|
||||
promise := t.promise()
|
||||
sleepQueueBaseTime += timeUnit(promise.data)
|
||||
sleepQueue = promise.next
|
||||
promise.state = TASK_STATE_RUNNABLE
|
||||
promise.next = nil
|
||||
runqueuePushBack(t)
|
||||
}
|
||||
@@ -271,9 +229,15 @@ func scheduler(main *coroutine) {
|
||||
}
|
||||
timeLeft := timeUnit(sleepQueue.promise().data) - (now - sleepQueueBaseTime)
|
||||
if schedulerDebug {
|
||||
println(" sleeping...", sleepQueue, uint32(timeLeft))
|
||||
println(" sleeping...", sleepQueue, uint(timeLeft))
|
||||
}
|
||||
sleepTicks(timeUnit(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
|
||||
}
|
||||
|
||||
@@ -281,8 +245,5 @@ func scheduler(main *coroutine) {
|
||||
scheduleLog(" <- runqueuePopFront")
|
||||
scheduleLogTask(" run:", t)
|
||||
t.resume()
|
||||
|
||||
// Add the just resumed task to the run queue or the sleep queue.
|
||||
yieldToScheduler(t)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user