Compare commits

...

9 Commits

Author SHA1 Message Date
Ayke van Laethem ea2f0e444d WIP stashed changes 2025-04-01 09:16:35 +02:00
Ayke van Laethem 04242fd620 WIP cores scheduler 2025-01-22 20:12:41 +01:00
Ayke van Laethem cce44b5ebb runtime: implement NumCPU for -scheduler=threads
For the threads scheduler, it makes sense to have NumCPU available.
For all other schedulers, the number of available CPUs is practically
limited to one by the scheduler (even though the system might have more
CPUs).
2025-01-21 11:11:42 +01:00
Ayke van Laethem 3767decf9e runtime: map every goroutine to a new OS thread
This is not a scheduler in the runtime, instead every goroutine is
mapped to a single OS thread - meaning 1:1 scheduling.

While this may not perform well (or at all) for large numbers of
threads, it greatly simplifies many things in the runtime. For example,
blocking syscalls can be called directly instead of having to use epoll
or similar. Also, we don't need to do anything special to call C code -
the default stack is all we need.
2025-01-21 10:48:01 +01:00
Ayke van Laethem 8d6e16019a sync: implement RWMutex using futexes
Somewhat surprisingly, this results in smaller code than the old code
with the cooperative (tasks) scheduler. Probably because the new RWMutex
is also simpler.
2025-01-21 10:48:01 +01:00
Ayke van Laethem 3cc9c44c26 runtime: refactor timerQueue
Move common functions to scheduler.go. They will be used both from the
cooperative and from the threads scheduler.
2025-01-21 10:47:56 +01:00
Ayke van Laethem c9bb33a73c runtime: make conservative and precise GC MT-safe
Using a global lock may be slow, but it is certainly simple and safe.
If this global lock becomes a bottleneck, we can of course look into
making the GC truly support multithreading.
2025-01-21 10:19:08 +01:00
Ayke van Laethem 8659b18d6f runtime: refactor GC mark phase into gcMarkReachable
This is a small refactor to prepare GC marking for multithreaded
stop-the-world.
2025-01-21 10:19:08 +01:00
Ayke van Laethem 2599db2990 internal/task: implement atomic primitives for preemptive scheduling 2025-01-21 10:19:08 +01:00
48 changed files with 1519 additions and 219 deletions
+1
View File
@@ -113,6 +113,7 @@ var libMusl = Library{
librarySources: func(target string) ([]string, error) {
arch := compileopts.MuslArchitecture(target)
globs := []string{
"conf/*.c",
"env/*.c",
"errno/*.c",
"exit/*.c",
+2 -2
View File
@@ -490,9 +490,9 @@ func loadProgramSize(path string, packagePathMap map[string]string) (*programSiz
continue
}
if section.Type == elf.SHT_NOBITS {
if section.Name == ".stack" {
if section.Name == ".stack" || section.Name == ".stack1" {
// TinyGo emits stack sections on microcontroller using the
// ".stack" name.
// ".stack" (or ".stack1") name.
// This is a bit ugly, but I don't think there is a way to
// mark the stack section in a linker script.
sections = append(sections, memorySection{
+5
View File
@@ -99,6 +99,11 @@ func (c *Config) BuildTags() []string {
"math_big_pure_go", // to get math/big to work
"gc." + c.GC(), "scheduler." + c.Scheduler(), // used inside the runtime package
"serial." + c.Serial()}...) // used inside the machine package
switch c.Scheduler() {
case "threads", "cores":
default:
tags = append(tags, "tinygo.unicore")
}
for i := 1; i <= c.GoMinorVersion; i++ {
tags = append(tags, fmt.Sprintf("go1.%d", i))
}
+1 -1
View File
@@ -10,7 +10,7 @@ import (
var (
validBuildModeOptions = []string{"default", "c-shared"}
validGCOptions = []string{"none", "leaking", "conservative", "custom", "precise"}
validSchedulerOptions = []string{"none", "tasks", "asyncify"}
validSchedulerOptions = []string{"none", "tasks", "asyncify", "threads", "cores"}
validSerialOptions = []string{"none", "uart", "usb", "rtt"}
validPrintSizeOptions = []string{"none", "short", "full", "html"}
validPanicStrategyOptions = []string{"print", "trap"}
+1 -1
View File
@@ -10,7 +10,7 @@ import (
func TestVerifyOptions(t *testing.T) {
expectedGCError := errors.New(`invalid gc option 'incorrect': valid values are none, leaking, conservative, custom, precise`)
expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, asyncify`)
expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, asyncify, threads`)
expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full, html`)
expectedPanicStrategyError := errors.New(`invalid panic option 'incorrect': valid values are print, trap`)
+5 -1
View File
@@ -248,7 +248,6 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
GOARCH: options.GOARCH,
BuildTags: []string{options.GOOS, options.GOARCH},
GC: "precise",
Scheduler: "tasks",
Linker: "cc",
DefaultStackSize: 1024 * 64, // 64kB
GDB: []string{"gdb"},
@@ -381,6 +380,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
platformVersion = "11.0.0" // first macosx platform with arm64 support
}
llvmvendor = "apple"
spec.Scheduler = "tasks"
spec.Linker = "ld.lld"
spec.Libc = "darwin-libSystem"
// Use macosx* instead of darwin, otherwise darwin/arm64 will refer to
@@ -398,6 +398,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
"src/runtime/runtime_unix.c",
"src/runtime/signal.c")
case "linux":
spec.Scheduler = "threads"
spec.Linker = "ld.lld"
spec.RTLib = "compiler-rt"
spec.Libc = "musl"
@@ -418,9 +419,11 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
}
spec.ExtraFiles = append(spec.ExtraFiles,
"src/internal/futex/futex_linux.c",
"src/internal/task/task_threads.c",
"src/runtime/runtime_unix.c",
"src/runtime/signal.c")
case "windows":
spec.Scheduler = "tasks"
spec.Linker = "ld.lld"
spec.Libc = "mingw-w64"
// Note: using a medium code model, low image base and no ASLR
@@ -498,6 +501,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
}
spec.ExtraFiles = append(spec.ExtraFiles, "src/runtime/asm_"+asmGoarch+suffix+".S")
spec.ExtraFiles = append(spec.ExtraFiles, "src/internal/task/task_stack_"+asmGoarch+suffix+".S")
spec.ExtraFiles = append(spec.ExtraFiles, "src/internal/task/task_cores.c")
}
// Configure the emulator.
+2
View File
@@ -208,6 +208,8 @@ func (c *compilerContext) getFunction(fn *ssa.Function) (llvm.Type, llvm.Value)
// > circumstances, and should not be exposed to source languages.
llvmutil.AppendToGlobal(c.mod, "llvm.compiler.used", llvmFn)
}
case "tinygo_exitTask", "tinygo_schedulerUnlock":
llvmutil.AppendToGlobal(c.mod, "llvm.used", llvmFn)
}
// External/exported functions may not retain pointer values.
+2
View File
@@ -1,3 +1,5 @@
//go:build tinygo.unicore
package task
// Atomics implementation for cooperative systems. The atomic types here aren't
+14
View File
@@ -0,0 +1,14 @@
//go:build !tinygo.unicore
package task
// Atomics implementation for non-cooperative systems (multithreaded, etc).
// These atomic types use real atomic instructions.
import "sync/atomic"
type (
Uintptr = atomic.Uintptr
Uint32 = atomic.Uint32
Uint64 = atomic.Uint64
)
+2
View File
@@ -1,3 +1,5 @@
//go:build tinygo.unicore
package task
// A futex is a way for userspace to wait with the pointer as the key, and for
+64
View File
@@ -0,0 +1,64 @@
//go:build scheduler.cores
package task
import "runtime/interrupt"
// A futex is a way for userspace to wait with the pointer as the key, and for
// another thread to wake one or all waiting threads keyed on the same pointer.
//
// A futex does not change the underlying value, it only reads it before to prevent
// lost wake-ups.
type Futex struct {
Uint32
waiters Stack
}
// Atomically check for cmp to still be equal to the futex value and if so, go
// to sleep. Return true if we were definitely awoken by a call to Wake or
// WakeAll, and false if we can't be sure of that.
func (f *Futex) Wait(cmp uint32) (awoken bool) {
mask := futexLock()
if f.Uint32.Load() != cmp {
futexUnlock(mask)
return false
}
// Push the current goroutine onto the waiter stack.
f.waiters.Push(Current())
futexUnlock(mask)
// Pause until this task is awoken by Wake/WakeAll.
Pause()
// We were awoken by a call to Wake or WakeAll. There is no chance for
// spurious wakeups.
return true
}
// Wake a single waiter.
func (f *Futex) Wake() {
mask := futexLock()
if t := f.waiters.Pop(); t != nil {
scheduleTask(t)
}
futexUnlock(mask)
}
// Wake all waiters.
func (f *Futex) WakeAll() {
mask := futexLock()
for t := f.waiters.Pop(); t != nil; t = f.waiters.Pop() {
scheduleTask(t)
}
futexUnlock(mask)
}
//go:linkname futexLock runtime.futexLock
func futexLock() interrupt.State
//go:linkname futexUnlock runtime.futexUnlock
func futexUnlock(interrupt.State)
+7
View File
@@ -0,0 +1,7 @@
//go:build scheduler.threads
package task
import "internal/futex"
type Futex = futex.Futex
+9
View File
@@ -0,0 +1,9 @@
//go:build linux && !baremetal
package task
import "unsafe"
// Musl uses a pointer (or unsigned long for C++) so unsafe.Pointer should be
// fine.
type threadID unsafe.Pointer
+2
View File
@@ -1,3 +1,5 @@
//go:build tinygo.unicore
package task
type Mutex struct {
+71
View File
@@ -0,0 +1,71 @@
//go:build !tinygo.unicore
package task
// Futex-based mutex.
// This is largely based on the paper "Futexes are Tricky" by Ulrich Drepper.
// It describes a few ways to implement mutexes using a futex, and how some
// seemingly-obvious implementations don't exactly work as intended.
// Unfortunately, Go atomic operations work slightly differently so we can't
// copy the algorithm verbatim.
//
// The implementation works like this. The futex can have 3 different values,
// depending on the state:
//
// - 0: the futex is currently unlocked.
// - 1: the futex is locked, but is uncontended. There is one special case: if
// a contended futex is unlocked, it is set to 0. It is possible for another
// thread to lock the futex before the next waiter is woken. But because a
// waiter will be woken (if there is one), it will always change to 2
// regardless. So this is not a problem.
// - 2: the futex is locked, and is contended. At least one thread is trying
// to obtain the lock (and is in the contended loop, see below).
//
// For the paper, see:
// https://dept-info.labri.fr/~denis/Enseignement/2008-IR/Articles/01-futex.pdf)
type Mutex struct {
futex Futex
}
func (m *Mutex) Lock() {
// Fast path: try to take an uncontended lock.
if m.futex.CompareAndSwap(0, 1) {
// We obtained the mutex.
return
}
// The futex is contended, so we enter the contended loop.
// If we manage to change the futex from 0 to 2, we managed to take the
// lock. Else, we have to wait until a call to Unlock unlocks this mutex.
// (Unlock will wake one waiter when it finds the futex is set to 2 when
// unlocking).
for m.futex.Swap(2) != 0 {
// Wait until we get resumed in Unlock.
m.futex.Wait(2)
}
}
func (m *Mutex) Unlock() {
if old := m.futex.Swap(0); old == 0 {
// Mutex wasn't locked before.
panic("sync: unlock of unlocked Mutex")
} else if old == 2 {
// Mutex was a contended lock, so we need to wake the next waiter.
m.futex.Wake()
}
}
// TryLock tries to lock m and reports whether it succeeded.
//
// Note that while correct uses of TryLock do exist, they are rare,
// and use of TryLock is often a sign of a deeper problem
// in a particular use of mutexes.
func (m *Mutex) TryLock() bool {
// Fast path: try to take an uncontended lock.
if m.futex.CompareAndSwap(0, 1) {
// We obtained the mutex.
return true
}
return false
}
+2
View File
@@ -1,3 +1,5 @@
//go:build tinygo.unicore
package task
// PMutex is a real mutex on systems that can be either preemptive or threaded,
+11
View File
@@ -0,0 +1,11 @@
//go:build !tinygo.unicore
package task
// PMutex is a real mutex on systems that can be either preemptive or threaded,
// and a dummy lock on other (purely cooperative) systems.
//
// It is mainly useful for short operations that need a lock when threading may
// be involved, but which do not need a lock with a purely cooperative
// scheduler.
type PMutex = Mutex
+32
View File
@@ -0,0 +1,32 @@
package task
// Barebones semaphore implementation.
// The main limitation is that if there are multiple waiters, a single Post()
// call won't do anything. Only when Post() has been called to awaken all
// waiters will the waiters proceed.
// This limitation is not a problem when there will only be a single waiter.
type Semaphore struct {
futex Futex
}
// Post (unlock) the semaphore, incrementing the value in the semaphore.
func (s *Semaphore) Post() {
newValue := s.futex.Add(1)
if newValue == 0 {
s.futex.WakeAll()
}
}
// Wait (lock) the semaphore, decrementing the value in the semaphore.
func (s *Semaphore) Wait() {
delta := int32(-1)
value := s.futex.Add(uint32(delta))
for {
if int32(value) >= 0 {
// Semaphore unlocked!
return
}
s.futex.Wait(value)
value = s.futex.Load()
}
}
+16
View File
@@ -21,11 +21,19 @@ type Task struct {
// state is the underlying running state of the task.
state state
RunState uint8
// DeferFrame stores a pointer to the (stack allocated) defer frame of the
// goroutine that is used for the recover builtin.
DeferFrame unsafe.Pointer
}
const (
RunStatePaused = iota
RunStateRunning
RunStateResuming
)
// DataUint32 returns the Data field as a uint32. The value is only valid after
// setting it through SetDataUint32 or by storing to it using DataAtomicUint32.
func (t *Task) DataUint32() uint32 {
@@ -53,3 +61,11 @@ func runtime_alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer
//go:linkname scheduleTask runtime.scheduleTask
func scheduleTask(*Task)
//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)))
-8
View File
@@ -6,14 +6,6 @@ import (
"unsafe"
)
// 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)))
//go:linkname runtimePanic runtime.runtimePanic
func runtimePanic(str string)
// state is a structure which holds a reference to the state of the task.
// When the task is suspended, the stack pointers are saved here.
type state struct {
-3
View File
@@ -7,9 +7,6 @@ import "unsafe"
// There is only one goroutine so the task struct can be a global.
var mainTask Task
//go:linkname runtimePanic runtime.runtimePanic
func runtimePanic(str string)
func Pause() {
runtimePanic("scheduler is disabled")
}
+27 -21
View File
@@ -1,4 +1,4 @@
//go:build scheduler.tasks
//go:build scheduler.tasks || scheduler.cores
package task
@@ -7,14 +7,6 @@ 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 {
@@ -33,40 +25,54 @@ type state struct {
}
// currentTask is the current running task, or nil if currently in the scheduler.
var currentTask *Task
//var currentTask *Task
// Current returns the current active task.
func Current() *Task {
return currentTask
//func Current() *Task {
// return currentTask
//}
//go:linkname Current runtime.currentTask
func Current() *Task
//go:linkname schedulerLock runtime.schedulerLock
func schedulerLock()
func Pause() {
schedulerLock()
PauseLocked()
}
// 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() {
func PauseLocked() {
// Check whether the canary (the lowest address of the stack) is still
// valid. If it is not, a stack overflow has occurred.
if *currentTask.state.canaryPtr != stackCanary {
if *Current().state.canaryPtr != stackCanary {
runtimePanic("goroutine stack overflow")
}
if interrupt.In() {
runtimePanic("blocked inside interrupt")
}
currentTask.state.pause()
current := Current()
current.RunState = RunStatePaused
current.state.pause()
}
//export tinygo_pause
func pause() {
//export tinygo_task_exit
func taskExit() {
println("-- exiting task")
Pause()
}
// Resume the task until it pauses or completes.
// This may only be called from the scheduler.
func (t *Task) Resume() {
currentTask = t
t.gcData.swap()
//currentTask = t
//t.gcData.swap()
t.state.resume()
t.gcData.swap()
currentTask = nil
//t.gcData.swap()
//currentTask = nil
}
// initialize the state and prepare to call the specified function with the specified argument bundle.
+1 -1
View File
@@ -28,7 +28,7 @@ tinygo_startTask:
blx r4
// After return, exit this goroutine. This is a tail call.
bl tinygo_pause
bl tinygo_task_exit
.cfi_endproc
.size tinygo_startTask, .-tinygo_startTask
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build scheduler.tasks && cortexm
//go:build (scheduler.tasks || scheduler.cores) && cortexm
package task
+113
View File
@@ -0,0 +1,113 @@
//go:build none
#define _GNU_SOURCE
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
// BDWGC also uses SIGRTMIN+6 on Linux, which seems like a reasonable choice.
#ifdef __linux__
#define taskPauseSignal (SIGRTMIN + 6)
#endif
// Pointer to the current task.Task structure.
// Ideally the entire task.Task structure would be a thread-local variable but
// this also works.
static __thread void *current_task;
struct state_pass {
void *(*start)(void*);
void *args;
void *task;
uintptr_t *stackTop;
sem_t startlock;
};
// Handle the GC pause in Go.
void tinygo_task_gc_pause(int sig);
// Initialize the main thread.
void tinygo_task_init(void *mainTask, pthread_t *thread, int *numCPU, void *context) {
// Make sure the current task pointer is set correctly for the main
// goroutine as well.
current_task = mainTask;
// Store the thread ID of the main thread.
*thread = pthread_self();
// Register the "GC pause" signal for the entire process.
// Using pthread_kill, we can still send the signal to a specific thread.
struct sigaction act = { 0 };
act.sa_flags = SA_SIGINFO;
act.sa_handler = &tinygo_task_gc_pause;
sigaction(taskPauseSignal, &act, NULL);
// Obtain the number of CPUs available on program start (for NumCPU).
int num = sysconf(_SC_NPROCESSORS_ONLN);
if (num <= 0) {
// Fallback in case there is an error.
num = 1;
}
*numCPU = num;
}
void tinygo_task_exited(void*);
// Helper to start a goroutine while also storing the 'task' structure.
static void* start_wrapper(void *arg) {
struct state_pass *state = arg;
void *(*start)(void*) = state->start;
void *args = state->args;
current_task = state->task;
// Save the current stack pointer in the goroutine state, for the GC.
int stackAddr;
*(state->stackTop) = (uintptr_t)(&stackAddr);
// Notify the caller that the thread has successfully started and
// initialized.
sem_post(&state->startlock);
// Run the goroutine function.
start(args);
// Notify the Go side this thread will exit.
tinygo_task_exited(current_task);
return NULL;
};
// Start a new goroutine in an OS thread.
int tinygo_task_start(uintptr_t fn, void *args, void *task, pthread_t *thread, uintptr_t *stackTop, void *context) {
// Sanity check. Should get optimized away.
if (sizeof(pthread_t) != sizeof(void*)) {
__builtin_trap();
}
struct state_pass state = {
.start = (void*)fn,
.args = args,
.task = task,
.stackTop = stackTop,
};
sem_init(&state.startlock, 0, 0);
int result = pthread_create(thread, NULL, &start_wrapper, &state);
// Wait until the thread has been created and read all state_pass variables.
sem_wait(&state.startlock);
return result;
}
// Return the current task (for task.Current()).
void* tinygo_task_current(void) {
return current_task;
}
// Send a signal to cause the task to pause for the GC mark phase.
void tinygo_task_send_gc_signal(pthread_t thread) {
pthread_kill(thread, taskPauseSignal);
}
+268
View File
@@ -0,0 +1,268 @@
//go:build scheduler.threads
package task
import (
"sync/atomic"
"unsafe"
)
// If true, print verbose debug logs.
const verbose = false
// Scheduler-specific state.
type state struct {
// Goroutine ID. The number here is not really significant and after a while
// it could wrap around. But it is useful for debugging.
id uintptr
// Thread ID, pthread_t or similar (typically implemented as a pointer).
thread threadID
// Highest address of the stack. It is stored when the goroutine starts, and
// is needed to be able to scan the stack.
stackTop uintptr
// Next task in the activeTasks queue.
QueueNext *Task
// Semaphore to pause/resume the thread atomically.
pauseSem Semaphore
// Semaphore used for stack scanning.
// We can't reuse pauseSem here since the thread might have been paused for
// other reasons (for example, because it was waiting on a channel).
gcSem Semaphore
}
// Goroutine counter, starting at 0 for the main goroutine.
var goroutineID uintptr
var numCPU int32
var mainTask Task
// Queue of tasks (see QueueNext) that currently exist in the program.
var activeTasks = &mainTask
var activeTaskLock PMutex
func OnSystemStack() bool {
runtimePanic("todo: task.OnSystemStack")
return false
}
// Initialize the main goroutine state. Must be called by the runtime on
// startup, before starting any other goroutines.
func Init(sp uintptr) {
mainTask.state.stackTop = sp
tinygo_task_init(&mainTask, &mainTask.state.thread, &numCPU)
}
// Return the task struct for the current thread.
func Current() *Task {
t := (*Task)(tinygo_task_current())
if t == nil {
runtimePanic("unknown current task")
}
return t
}
// Pause pauses the current task, until it is resumed by another task.
// It is possible that another task has called Resume() on the task before it
// hits Pause(), in which case the task won't be paused but continues
// immediately.
func Pause() {
// Wait until resumed
t := Current()
if verbose {
println("*** pause: ", t.state.id)
}
t.state.pauseSem.Wait()
}
// Resume the given task.
// It is legal to resume a task before it gets paused, it means that the next
// call to Pause() won't pause but will continue immediately. This happens in
// practice sometimes in channel operations, where the Resume() might get called
// between the channel unlock and the call to Pause().
func (t *Task) Resume() {
if verbose {
println("*** resume: ", t.state.id)
}
// Increment the semaphore counter.
// If the task is currently paused in Wait(), it will resume.
// If the task is not yet paused, the next call to Wait() will continue
// immediately.
t.state.pauseSem.Post()
}
// Start a new OS thread.
func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
t := &Task{}
t.state.id = atomic.AddUintptr(&goroutineID, 1)
if verbose {
println("*** start: ", t.state.id, "from", Current().state.id)
}
// Start the new thread, and add it to the list of threads.
// Do this with a lock so that only started threads are part of the queue
// and the stop-the-world GC won't see threads that haven't started yet or
// are not fully started yet.
activeTaskLock.Lock()
errCode := tinygo_task_start(fn, args, t, &t.state.thread, &t.state.stackTop)
if errCode != 0 {
runtimePanic("could not start thread")
}
t.state.QueueNext = activeTasks
activeTasks = t
activeTaskLock.Unlock()
}
//export tinygo_task_exited
func taskExited(t *Task) {
if verbose {
println("*** exit:", t.state.id)
}
// Remove from the queue.
// TODO: this can be made more efficient by using a doubly linked list.
activeTaskLock.Lock()
found := false
for q := &activeTasks; *q != nil; q = &(*q).state.QueueNext {
if *q == t {
*q = t.state.QueueNext
found = true
break
}
}
activeTaskLock.Unlock()
// Sanity check.
if !found {
runtimePanic("taskExited failed")
}
}
// Futex to wait on until all tasks have finished scanning the stack.
// This is basically a sync.WaitGroup.
var scanDoneFutex Futex
// GC scan phase. Because we need to stop the world while scanning, this kinda
// needs to be done in the tasks package.
func GCScan() {
current := Current()
// Don't allow new goroutines to be started while pausing/resuming threads
// in the stop-the-world phase.
activeTaskLock.Lock()
// Pause all other threads.
numOtherThreads := uint32(0)
for t := activeTasks; t != nil; t = t.state.QueueNext {
if t != current {
numOtherThreads++
tinygo_task_send_gc_signal(t.state.thread)
}
}
// Store the number of threads to wait for in the futex.
// This is the equivalent of doing an initial wg.Add(numOtherThreads).
scanDoneFutex.Store(numOtherThreads)
// Scan the current stack, and all current registers.
scanCurrentStack()
// Wake each paused thread for the first time so it will scan the stack.
for t := activeTasks; t != nil; t = t.state.QueueNext {
if t != current {
t.state.gcSem.Post()
}
}
// Wait until all threads have finished scanning their stack.
// This is the equivalent of wg.Wait()
for {
val := scanDoneFutex.Load()
if val == 0 {
break
}
scanDoneFutex.Wait(val)
}
// Scan all globals (implemented in the runtime).
gcScanGlobals()
// Wake each paused thread for the second time, so they will resume normal
// operation.
for t := activeTasks; t != nil; t = t.state.QueueNext {
if t != current {
t.state.gcSem.Post()
}
}
// Allow goroutines to start and exit again.
activeTaskLock.Unlock()
}
// Scan globals, implemented in the runtime package.
func gcScanGlobals()
var stackScanLock PMutex
//export tinygo_task_gc_pause
func tingyo_task_gc_pause() {
// Wait until we get the signal to start scanning the stack.
Current().state.gcSem.Wait()
// Scan the thread stack.
// Only scan a single thread stack at a time, because the GC marking phase
// doesn't support parallelism.
// TODO: it may be possible to call markRoots directly (without saving
// registers) since we are in a signal handler that already saved a bunch of
// registers. This is an optimization left for a future time.
stackScanLock.Lock()
scanCurrentStack()
stackScanLock.Unlock()
// Equivalent of wg.Done(): subtract one from the futex and if the result is
// 0 (meaning we were the last in the waitgroup), wake the waiting thread.
n := uint32(1)
if scanDoneFutex.Add(-n) == 0 {
scanDoneFutex.Wake()
}
// Wait until we get the signal we can resume normally (after the mark phase
// has finished).
Current().state.gcSem.Wait()
}
//go:export tinygo_scanCurrentStack
func scanCurrentStack()
// Return the highest address of the current stack.
func StackTop() uintptr {
return Current().state.stackTop
}
// Using //go:linkname instead of //export so that we don't tell the compiler
// that the 't' parameter won't escape (because it will).
//
//go:linkname tinygo_task_init tinygo_task_init
func tinygo_task_init(t *Task, thread *threadID, numCPU *int32)
// Here same as for tinygo_task_init.
//
//go:linkname tinygo_task_start tinygo_task_start
func tinygo_task_start(fn uintptr, args unsafe.Pointer, t *Task, thread *threadID, stackTop *uintptr) int32
// Pause the thread by sending it a signal.
//
//export tinygo_task_send_gc_signal
func tinygo_task_send_gc_signal(threadID)
//export tinygo_task_current
func tinygo_task_current() unsafe.Pointer
func NumCPU() int {
return int(numCPU)
}
+30 -31
View File
@@ -6,7 +6,6 @@
package runtime
import (
"runtime/interrupt"
_ "unsafe"
)
@@ -23,27 +22,27 @@ import (
func __atomic_load_2(ptr *uint16, ordering uintptr) uint16 {
// The LLVM docs for this say that there is a val argument after the pointer.
// That is a typo, and the GCC docs omit it.
mask := interrupt.Disable()
mask := atomicLock()
val := *ptr
interrupt.Restore(mask)
atomicUnlock(mask)
return val
}
//export __atomic_store_2
func __atomic_store_2(ptr *uint16, val uint16, ordering uintptr) {
mask := interrupt.Disable()
mask := atomicLock()
*ptr = val
interrupt.Restore(mask)
atomicUnlock(mask)
}
//go:inline
func doAtomicCAS16(ptr *uint16, expected, desired uint16) uint16 {
mask := interrupt.Disable()
mask := atomicLock()
old := *ptr
if old == expected {
*ptr = desired
}
interrupt.Restore(mask)
atomicUnlock(mask)
return old
}
@@ -61,10 +60,10 @@ func __atomic_compare_exchange_2(ptr, expected *uint16, desired uint16, successO
//go:inline
func doAtomicSwap16(ptr *uint16, new uint16) uint16 {
mask := interrupt.Disable()
mask := atomicLock()
old := *ptr
*ptr = new
interrupt.Restore(mask)
atomicUnlock(mask)
return old
}
@@ -80,11 +79,11 @@ func __atomic_exchange_2(ptr *uint16, new uint16, ordering uintptr) uint16 {
//go:inline
func doAtomicAdd16(ptr *uint16, value uint16) (old, new uint16) {
mask := interrupt.Disable()
mask := atomicLock()
old = *ptr
new = old + value
*ptr = new
interrupt.Restore(mask)
atomicUnlock(mask)
return old, new
}
@@ -112,27 +111,27 @@ func __atomic_add_fetch_2(ptr *uint16, value uint16, ordering uintptr) uint16 {
func __atomic_load_4(ptr *uint32, ordering uintptr) uint32 {
// The LLVM docs for this say that there is a val argument after the pointer.
// That is a typo, and the GCC docs omit it.
mask := interrupt.Disable()
mask := atomicLock()
val := *ptr
interrupt.Restore(mask)
atomicUnlock(mask)
return val
}
//export __atomic_store_4
func __atomic_store_4(ptr *uint32, val uint32, ordering uintptr) {
mask := interrupt.Disable()
mask := atomicLock()
*ptr = val
interrupt.Restore(mask)
atomicUnlock(mask)
}
//go:inline
func doAtomicCAS32(ptr *uint32, expected, desired uint32) uint32 {
mask := interrupt.Disable()
mask := atomicLock()
old := *ptr
if old == expected {
*ptr = desired
}
interrupt.Restore(mask)
atomicUnlock(mask)
return old
}
@@ -150,10 +149,10 @@ func __atomic_compare_exchange_4(ptr, expected *uint32, desired uint32, successO
//go:inline
func doAtomicSwap32(ptr *uint32, new uint32) uint32 {
mask := interrupt.Disable()
mask := atomicLock()
old := *ptr
*ptr = new
interrupt.Restore(mask)
atomicUnlock(mask)
return old
}
@@ -169,11 +168,11 @@ func __atomic_exchange_4(ptr *uint32, new uint32, ordering uintptr) uint32 {
//go:inline
func doAtomicAdd32(ptr *uint32, value uint32) (old, new uint32) {
mask := interrupt.Disable()
mask := atomicLock()
old = *ptr
new = old + value
*ptr = new
interrupt.Restore(mask)
atomicUnlock(mask)
return old, new
}
@@ -201,27 +200,27 @@ func __atomic_add_fetch_4(ptr *uint32, value uint32, ordering uintptr) uint32 {
func __atomic_load_8(ptr *uint64, ordering uintptr) uint64 {
// The LLVM docs for this say that there is a val argument after the pointer.
// That is a typo, and the GCC docs omit it.
mask := interrupt.Disable()
mask := atomicLock()
val := *ptr
interrupt.Restore(mask)
atomicUnlock(mask)
return val
}
//export __atomic_store_8
func __atomic_store_8(ptr *uint64, val uint64, ordering uintptr) {
mask := interrupt.Disable()
mask := atomicLock()
*ptr = val
interrupt.Restore(mask)
atomicUnlock(mask)
}
//go:inline
func doAtomicCAS64(ptr *uint64, expected, desired uint64) uint64 {
mask := interrupt.Disable()
mask := atomicLock()
old := *ptr
if old == expected {
*ptr = desired
}
interrupt.Restore(mask)
atomicUnlock(mask)
return old
}
@@ -239,10 +238,10 @@ func __atomic_compare_exchange_8(ptr, expected *uint64, desired uint64, successO
//go:inline
func doAtomicSwap64(ptr *uint64, new uint64) uint64 {
mask := interrupt.Disable()
mask := atomicLock()
old := *ptr
*ptr = new
interrupt.Restore(mask)
atomicUnlock(mask)
return old
}
@@ -258,11 +257,11 @@ func __atomic_exchange_8(ptr *uint64, new uint64, ordering uintptr) uint64 {
//go:inline
func doAtomicAdd64(ptr *uint64, value uint64) (old, new uint64) {
mask := interrupt.Disable()
mask := atomicLock()
old = *ptr
new = old + value
*ptr = new
interrupt.Restore(mask)
atomicUnlock(mask)
return old, new
}
+13
View File
@@ -0,0 +1,13 @@
//go:build baremetal && !tinygo.unicore
package runtime
import "runtime/interrupt"
func atomicLock() interrupt.State {
return atomicLockImpl()
}
func atomicUnlock(mask interrupt.State) {
atomicUnlockImpl(mask)
}
+13
View File
@@ -0,0 +1,13 @@
//go:build baremetal && tinygo.unicore
package runtime
import "runtime/interrupt"
func atomicLock() interrupt.State {
return interrupt.Disable()
}
func atomicUnlock(mask interrupt.State) {
interrupt.Restore(mask)
}
-9
View File
@@ -1,14 +1,5 @@
package runtime
// NumCPU returns the number of logical CPUs usable by the current process.
//
// The set of available CPUs is checked by querying the operating system
// at process startup. Changes to operating system CPU allocation after
// process startup are not reflected.
func NumCPU() int {
return 1
}
// Stub for NumCgoCall, does not return the real value
func NumCgoCall() int {
return 0
+13 -2
View File
@@ -57,6 +57,7 @@ var (
gcMallocs uint64 // total number of allocations
gcFrees uint64 // total number of objects freed
gcFreedBlocks uint64 // total number of freed blocks
gcLock task.PMutex // lock to avoid race conditions on multicore systems
)
// zeroSizedAlloc is just a sentinel that gets returned when allocating 0 bytes.
@@ -317,6 +318,10 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
runtimePanicAt(returnAddress(0), "heap alloc in interrupt")
}
// Make sure there are no concurrent allocations. The heap is not currently
// designed for concurrent alloc/GC.
gcLock.Lock()
gcTotalAlloc += uint64(size)
gcMallocs++
@@ -399,6 +404,9 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
i.setState(blockStateTail)
}
// We've claimed this allocation, now we can unlock the heap.
gcLock.Unlock()
// Return a pointer to this allocation.
pointer := thisAlloc.pointer()
if preciseHeap {
@@ -444,7 +452,9 @@ func free(ptr unsafe.Pointer) {
// GC performs a garbage collection cycle.
func GC() {
gcLock.Lock()
runGC()
gcLock.Unlock()
}
// runGC performs a garbage collection cycle. It is the internal implementation
@@ -456,8 +466,7 @@ func runGC() (freeBytes uintptr) {
}
// Mark phase: mark all reachable objects, recursively.
markStack()
findGlobals(markRoots)
gcMarkReachable()
if baremetal && hasScheduler {
// Channel operations in interrupts may move task pointers around while we are marking.
@@ -714,6 +723,7 @@ func dumpHeap() {
// The returned memory statistics are up to date as of the
// call to ReadMemStats. This would not do GC implicitly for you.
func ReadMemStats(m *MemStats) {
gcLock.Lock()
m.HeapIdle = 0
m.HeapInuse = 0
for block := gcBlock(0); block < endBlock; block++ {
@@ -733,6 +743,7 @@ func ReadMemStats(m *MemStats) {
m.Sys = uint64(heapEnd - heapStart)
m.HeapAlloc = (gcTotalBlocks - gcFreedBlocks) * uint64(bytesPerBlock)
m.Alloc = m.HeapAlloc
gcLock.Unlock()
}
func SetFinalizer(obj interface{}, finalizer interface{}) {
+5
View File
@@ -8,6 +8,11 @@ import (
"unsafe"
)
func gcMarkReachable() {
markStack()
findGlobals(markRoots)
}
//go:extern runtime.stackChainStart
var stackChainStart *stackChainObject
+6 -1
View File
@@ -1,9 +1,14 @@
//go:build (gc.conservative || gc.precise) && !tinygo.wasm
//go:build (gc.conservative || gc.precise) && !tinygo.wasm && !scheduler.threads && !scheduler.cores
package runtime
import "internal/task"
func gcMarkReachable() {
markStack()
findGlobals(markRoots)
}
// markStack marks all root pointers found on the stack.
//
// This implementation is conservative and relies on the stack top (provided by
+25
View File
@@ -0,0 +1,25 @@
//go:build scheduler.threads
package runtime
import "internal/task"
func gcMarkReachable() {
task.GCScan()
}
// Scan globals inside the stop-the-world phase. Called from the STW
// implementation in the internal/task package.
//
//go:linkname gcScanGlobals internal/task.gcScanGlobals
func gcScanGlobals() {
findGlobals(markRoots)
}
// Function called from assembly with all registers pushed, to actually scan the
// stack.
//
//go:export tinygo_scanstack
func scanstack(sp uintptr) {
markRoots(sp, task.StackTop())
}
+4
View File
@@ -13,11 +13,15 @@ type stringer interface {
// This is a no-op lock on systems that do not have parallelism.
var printLock task.PMutex
//var printLocked interrupt.State
func printlock() {
//printLocked = serialLock()
printLock.Lock()
}
func printunlock() {
//serialUnlock(printLocked)
printLock.Unlock()
}
+30
View File
@@ -15,6 +15,8 @@ type timeUnit int64
//go:linkname systemInit SystemInit
func systemInit()
const numCPU = 1
//export Reset_Handler
func main() {
if nrf.FPUPresent {
@@ -145,3 +147,31 @@ func rtc_sleep(ticks uint32) {
waitForEvents()
}
}
func atomicLockImpl() interrupt.State {
mask := interrupt.Disable()
return mask
}
func atomicUnlockImpl(mask interrupt.State) {
interrupt.Restore(mask)
}
func futexLock() interrupt.State {
mask := interrupt.Disable()
return mask
}
func futexUnlock(mask interrupt.State) {
interrupt.Restore(mask)
}
func schedulerLock() {
}
func schedulerUnlock() {
}
func currentCPU() uint32 {
return 0
}
+184
View File
@@ -4,8 +4,13 @@ package runtime
import (
"device/arm"
"device/rp"
"internal/task"
"machine"
"machine/usb/cdc"
"reflect"
"runtime/interrupt"
"unsafe"
)
// machineTicks is provided by package machine.
@@ -16,6 +21,8 @@ func machineLightSleep(uint64)
type timeUnit int64
const numCPU = 2
// ticks returns the number of ticks (microseconds) elapsed since power up.
func ticks() timeUnit {
t := machineTicks()
@@ -50,14 +57,18 @@ func waitForEvents() {
}
func putchar(c byte) {
//mask := serialLock()
machine.Serial.WriteByte(c)
//serialUnlock(mask)
}
func getchar() byte {
mask := serialLock()
for machine.Serial.Buffered() == 0 {
Gosched()
}
v, _ := machine.Serial.ReadByte()
serialUnlock(mask)
return v
}
@@ -71,9 +82,11 @@ func machineInit()
func init() {
machineInit()
mask := serialLock()
cdc.EnableUSBCDC()
machine.USBDev.Configure(machine.UARTConfig{})
machine.InitSerial()
serialUnlock(mask)
}
//export Reset_Handler
@@ -82,3 +95,174 @@ func main() {
run()
exit(0)
}
func multicore_fifo_rvalid() bool {
return rp.SIO.FIFO_ST.Get()&rp.SIO_FIFO_ST_VLD != 0
}
func multicore_fifo_wready() bool {
return rp.SIO.FIFO_ST.Get()&rp.SIO_FIFO_ST_RDY != 0
}
func multicore_fifo_drain() {
for multicore_fifo_rvalid() {
rp.SIO.FIFO_RD.Get()
}
}
func multicore_fifo_push_blocking(data uint32) {
for !multicore_fifo_wready() {
}
rp.SIO.FIFO_WR.Set(data)
arm.Asm("sev")
}
func multicore_fifo_pop_blocking() uint32 {
for !multicore_fifo_rvalid() {
arm.Asm("wfe")
}
return rp.SIO.FIFO_RD.Get()
}
//go:extern __isr_vector
var __isr_vector [0]uint32
//go:extern _stack1_top
var _stack1_top [0]uint32
var core1StartSequence = [...]uint32{
0, 0, 1,
uint32(uintptr(unsafe.Pointer(&__isr_vector))),
uint32(uintptr(unsafe.Pointer(&_stack1_top))),
uint32(uintptr(reflect.ValueOf(runCore1).Pointer())),
}
func startSecondaryCores() {
// Start the second core of the RP2040.
// See section 2.8.2 in the datasheet.
seq := 0
for {
cmd := core1StartSequence[seq]
if cmd == 0 {
multicore_fifo_drain()
arm.Asm("sev")
}
multicore_fifo_push_blocking(cmd)
response := multicore_fifo_pop_blocking()
if cmd != response {
seq = 0
continue
}
seq = seq + 1
if seq >= len(core1StartSequence) {
break
}
}
}
var core1Task task.Task
func runCore1() {
//until := ticks() + nanosecondsToTicks(1900e6)
//for ticks() < until {
//}
println("starting core 1")
runSecondary(1, &core1Task)
// Just blink a LED to show that this core is running.
// TODO: use a real scheduler.
//led := machine.GP16
//led.Configure(machine.PinConfig{Mode: machine.PinOutput})
//const cycles = 7000_000
//for {
// for i := 0; i < cycles; i++ {
// led.Low()
// }
// for i := 0; i < cycles; i++ {
// led.High()
// }
//}
}
func currentCPU() uint32 {
return rp.SIO.CPUID.Get()
}
const (
spinlockAtomic = iota
spinlockFutex
spinlockScheduler
)
func atomicLockImpl() interrupt.State {
mask := interrupt.Disable()
for rp.SIO.SPINLOCK0.Get() == 0 {
}
return mask
}
func atomicUnlockImpl(mask interrupt.State) {
rp.SIO.SPINLOCK0.Set(0)
interrupt.Restore(mask)
}
func futexLock() interrupt.State {
// Disable interrupts.
// This is necessary since we might do some futex operations (like Wake)
// inside an interrupt and we don't want to deadlock with a non-interrupt
// goroutine that has taken the spinlock at the same time.
mask := interrupt.Disable()
// Acquire the spinlock.
for rp.SIO.SPINLOCK1.Get() == 0 {
// Spin, until the lock is released.
}
return mask
}
func futexUnlock(mask interrupt.State) {
// Release the spinlock.
rp.SIO.SPINLOCK1.Set(0)
// Restore interrupts.
interrupt.Restore(mask)
}
var schedulerLockMasks [numCPU]interrupt.State
var schedulerLocked = false
// WARNING: doesn't check for deadlocks!
func schedulerLock() {
//schedulerLockMasks[currentCPU()] = interrupt.Disable()
for rp.SIO.SPINLOCK2.Get() == 0 {
}
schedulerLocked = true
}
func schedulerUnlock() {
if !schedulerLocked {
println("!!! not locked at unlock")
for {
}
}
schedulerLocked = false
rp.SIO.SPINLOCK2.Set(0)
//interrupt.Restore(schedulerLockMasks[currentCPU()])
}
func serialLock() interrupt.State {
//mask := interrupt.Disable()
for rp.SIO.SPINLOCK3.Get() == 0 {
}
//return mask
return 0
}
func serialUnlock(mask interrupt.State) {
rp.SIO.SPINLOCK3.Set(0)
//interrupt.Restore(mask)
}
+7
View File
@@ -11,6 +11,12 @@ import (
"unsafe"
)
const numCPU = 1
func currentCPU() int {
return 0
}
//export write
func libc_write(fd int32, buf unsafe.Pointer, count uint) int
@@ -73,6 +79,7 @@ type timespec struct {
tv_nsec int64 // unsigned 64-bit integer on all time64 platforms
}
// Highest address of the stack of the main thread.
var stackTop uintptr
// Entry point for Go. Initialize all packages and call main.main().
+30
View File
@@ -6,6 +6,8 @@ const schedulerDebug = false
var mainExited bool
var timerQueue *timerNode
// Simple logging, for debugging.
func scheduleLog(msg string) {
if schedulerDebug {
@@ -27,6 +29,34 @@ func scheduleLogChan(msg string, ch *channel, t *task.Task) {
}
}
func timerQueueAdd(tn *timerNode) {
q := &timerQueue
for ; *q != nil; q = &(*q).next {
if tn.whenTicks() < (*q).whenTicks() {
// this will finish earlier than the next - insert here
break
}
}
tn.next = *q
*q = tn
}
func timerQueueRemove(t *timer) bool {
removedTimer := false
for q := &timerQueue; *q != nil; q = &(*q).next {
if (*q).timer == t {
scheduleLog("removed timer")
*q = (*q).next
removedTimer = true
break
}
}
if !removedTimer {
scheduleLog("did not remove timer")
}
return removedTimer
}
// Goexit terminates the currently running goroutine. No other goroutines are affected.
func Goexit() {
panicOrGoexit(nil, panicGoexit)
+7 -24
View File
@@ -32,7 +32,6 @@ var (
runqueue task.Queue
sleepQueue *task.Task
sleepQueueBaseTime timeUnit
timerQueue *timerNode
)
// deadlock is called when a goroutine cannot proceed any more, but is in theory
@@ -58,6 +57,11 @@ func Gosched() {
task.Pause()
}
// NumCPU returns the number of logical CPUs usable by the current process.
func NumCPU() int {
return 1
}
// Add this task to the sleep queue, assuming its state is set to sleeping.
func addSleepTask(t *task.Task, duration timeUnit) {
if schedulerDebug {
@@ -100,36 +104,15 @@ func addSleepTask(t *task.Task, duration timeUnit) {
// sleepQueue.
func addTimer(tim *timerNode) {
mask := interrupt.Disable()
// Add to timer queue.
q := &timerQueue
for ; *q != nil; q = &(*q).next {
if tim.whenTicks() < (*q).whenTicks() {
// this will finish earlier than the next - insert here
break
}
}
tim.next = *q
*q = tim
timerQueueAdd(tim)
interrupt.Restore(mask)
}
// removeTimer is the implementation of time.stopTimer. It removes a timer from
// the timer queue, returning true if the timer is present in the timer queue.
func removeTimer(tim *timer) bool {
removedTimer := false
mask := interrupt.Disable()
for t := &timerQueue; *t != nil; t = &(*t).next {
if (*t).timer == tim {
scheduleLog("removed timer")
*t = (*t).next
removedTimer = true
break
}
}
if !removedTimer {
scheduleLog("did not remove timer")
}
removedTimer := timerQueueRemove(tim)
interrupt.Restore(mask)
return removedTimer
}
+262
View File
@@ -0,0 +1,262 @@
//go:build scheduler.cores
package runtime
import (
"device/arm"
"internal/task"
)
const hasScheduler = true
const hasParallelism = true
const coresVerbose = false
var (
mainTask task.Task
cpuTasks [numCPU]*task.Task
sleepQueue *task.Task
runQueue *task.Task
)
func deadlock() {
// Call yield without requesting a wakeup.
task.Pause()
trap()
}
func scheduleTask(t *task.Task) {
schedulerLock()
switch t.RunState {
case task.RunStatePaused:
// Paused, state is saved on the stack.
if coresVerbose {
println("## schedule: add to runQueue")
}
addToRunQueue(t)
arm.Asm("sev")
case task.RunStateRunning:
// Not yet paused (probably going to pause very soon), so let the
// Pause() function know it can resume immediately.
t.RunState = task.RunStateResuming
if coresVerbose {
println("## schedule: mark as resuming")
}
default:
println("Unknown run state??")
for {
}
}
schedulerUnlock()
}
// Add task to runQueue.
// Scheduler lock must be held when calling this function.
func addToRunQueue(t *task.Task) {
t.Next = runQueue
runQueue = t
}
func addSleepTask(t *task.Task, wakeup timeUnit) {
// Save the timestamp when the task should be woken up.
t.Data = uint64(wakeup)
// Find the position where we should insert this task in the queue.
q := &sleepQueue
for {
if *q == nil {
// Found the end of the time queue. Insert it here, at the end.
break
}
if timeUnit((*q).Data) > timeUnit(t.Data) {
// Found a task in the queue that has a timeout before the
// to-be-sleeping task. Insert our task right before.
break
}
q = &(*q).Next
}
// Insert the task into the queue (this could be at the end, if *q is nil).
t.Next = *q
*q = t
}
func Gosched() {
// TODO
}
// NumCPU returns the number of logical CPUs usable by the current process.
func NumCPU() int {
// Return the hardcoded number of physical CPU cores.
return numCPU
}
func addTimer(tn *timerNode) {
runtimePanic("todo: timers")
}
func removeTimer(t *timer) bool {
runtimePanic("todo: timers")
return false
}
func schedulerRunQueue() *task.Queue {
println("todo: schedulerRunQueue")
for {
}
return nil
}
// Pause the current task for a given time.
//
//go:linkname sleep time.Sleep
func sleep(duration int64) {
if duration <= 0 {
return
}
wakeup := ticks() + nanosecondsToTicks(duration)
schedulerLock()
addSleepTask(task.Current(), wakeup)
task.PauseLocked()
}
func run() {
initHeap()
cpuTasks[0] = &mainTask
initAll() // TODO: move into main goroutine!
until := ticks() + nanosecondsToTicks(200e6)
for ticks() < until {
}
println("\n\n=====")
go func() {
//initAll()
startSecondaryCores()
callMain()
mainExited = true
}()
schedulerLock()
scheduler()
}
func runSecondary(core uint32, t *task.Task) {
println("-- runSecondary")
cpuTasks[core] = t
println("-- locking for 2nd core")
schedulerLock()
println("-- locked!")
scheduler()
}
var schedulerIsRunning = false
func scheduler() {
if coresVerbose {
println("** scheduler on core:", currentCPU())
}
for {
//until := ticks() + nanosecondsToTicks(100e6)
//for ticks() < until {
//}
// Check for ready-to-run tasks.
if runnable := runQueue; runnable != nil {
if coresVerbose {
println("** scheduler", currentCPU(), "run runnable")
}
// Pop off the run queue.
runQueue = runnable.Next
runnable.Next = nil
// Resume it now.
setCurrentTask(runnable)
schedulerUnlock()
runnable.Resume()
if coresVerbose {
println("** scheduler", currentCPU(), " returned (from runqueue resume)")
}
setCurrentTask(nil)
continue
}
// If another core is using the clock, let it handle the sleep queue.
if schedulerIsRunning {
if coresVerbose {
println("** scheduler", currentCPU(), "wait for other core")
}
schedulerUnlock()
waitForEvents()
schedulerLock()
continue
}
if sleepingTask := sleepQueue; sleepingTask != nil {
now := ticks()
if now >= timeUnit(sleepingTask.Data) {
if coresVerbose {
println("** scheduler", currentCPU(), "run sleeping")
}
// This task is done sleeping.
// Resume it now.
sleepQueue = sleepQueue.Next
sleepingTask.Next = nil
setCurrentTask(sleepingTask)
schedulerUnlock()
sleepingTask.Resume()
if coresVerbose {
println("** scheduler", currentCPU(), " returned (from sleepQueue resume)")
}
setCurrentTask(nil)
continue
}
delay := timeUnit(sleepingTask.Data) - now
if coresVerbose {
println("** scheduler", currentCPU(), "sleep", ticksToNanoseconds(delay)/1e6)
}
// Sleep for a bit until the next task is ready to run.
schedulerIsRunning = true
schedulerUnlock()
sleepTicks(delay)
schedulerLock()
schedulerIsRunning = false
continue
}
if coresVerbose {
println("** scheduler", currentCPU(), "wait for events")
}
schedulerUnlock()
waitForEvents()
schedulerLock()
}
}
func currentTask() *task.Task {
return cpuTasks[currentCPU()]
}
func setCurrentTask(task *task.Task) {
cpuTasks[currentCPU()] = task
}
func runtimeTicks() uint64 {
return uint64(ticks())
}
func runtimeSleepTicks(delay uint64) {
sleepTicks(timeUnit(delay))
}
//export tinygo_schedulerUnlock
func tinygo_schedulerUnlock() {
schedulerUnlock()
}
+5
View File
@@ -40,6 +40,11 @@ func Gosched() {
// There are no other goroutines, so there's nothing to schedule.
}
// NumCPU returns the number of logical CPUs usable by the current process.
func NumCPU() int {
return 1
}
func addTimer(tim *timerNode) {
runtimePanic("timers not supported without a scheduler")
}
+129
View File
@@ -0,0 +1,129 @@
//go:build scheduler.threads
package runtime
import "internal/task"
const hasScheduler = false // not using the cooperative scheduler
// We use threads, so yes there is parallelism.
const hasParallelism = true
var (
timerQueueLock task.PMutex
timerQueueStarted bool
timerFutex task.Futex
)
// Because we just use OS threads, we don't need to do anything special here. We
// can just initialize everything and run main.main on the main thread.
func run() {
initHeap()
task.Init(stackTop)
initAll()
callMain()
}
// Pause the current task for a given time.
//
//go:linkname sleep time.Sleep
func sleep(duration int64) {
if duration <= 0 {
return
}
sleepTicks(nanosecondsToTicks(duration))
}
func deadlock() {
// TODO: exit the thread via pthread_exit.
task.Pause()
}
func scheduleTask(t *task.Task) {
t.Resume()
}
func Gosched() {
// Each goroutine runs in a thread, so there's not much we can do here.
// There is sched_yield but it's only really intended for realtime
// operation, so is probably best not to use.
}
// NumCPU returns the number of logical CPUs usable by the current process.
func NumCPU() int {
return task.NumCPU()
}
// Separate goroutine (thread) that runs timer callbacks when they expire.
func timerRunner() {
for {
timerQueueLock.Lock()
if timerQueue == nil {
// No timer in the queue, so wait until one becomes available.
val := timerFutex.Load()
timerQueueLock.Unlock()
timerFutex.Wait(val)
continue
}
now := ticks()
if now < timerQueue.whenTicks() {
// There is a timer in the queue, but we need to wait until it
// expires.
// Using a futex, so that the wait is exited early when adding a new
// (sooner-to-expire) timer.
val := timerFutex.Load()
timerQueueLock.Unlock()
timeout := ticksToNanoseconds(timerQueue.whenTicks() - now)
timerFutex.WaitUntil(val, uint64(timeout))
continue
}
// Pop timer from queue.
tn := timerQueue
timerQueue = tn.next
tn.next = nil
timerQueueLock.Unlock()
// Run the callback stored in this timer node.
delay := ticksToNanoseconds(now - tn.whenTicks())
tn.callback(tn, delay)
}
}
func addTimer(tim *timerNode) {
timerQueueLock.Lock()
if !timerQueueStarted {
timerQueueStarted = true
go timerRunner()
}
timerQueueAdd(tim)
timerFutex.Add(1)
timerFutex.Wake()
timerQueueLock.Unlock()
}
func removeTimer(tim *timer) bool {
timerQueueLock.Lock()
removed := timerQueueRemove(tim)
timerQueueLock.Unlock()
return removed
}
func schedulerRunQueue() *task.Queue {
// This function is not actually used, it is only called when hasScheduler
// is true. So we can just return nil here.
return nil
}
func runqueueForGC() *task.Queue {
// There is only a runqueue when using the cooperative scheduler.
return nil
}
+62 -97
View File
@@ -6,131 +6,96 @@ import (
type Mutex = task.Mutex
//go:linkname runtimePanic runtime.runtimePanic
func runtimePanic(msg string)
type RWMutex struct {
// waitingWriters are all of the tasks waiting for write locks.
waitingWriters task.Stack
// Reader count, with the number of readers that currently have read-locked
// this mutex.
// The value can be in two states: one where 0 means no readers and another
// where -rwMutexMaxReaders means no readers. A base of 0 is normal
// uncontended operation, a base of -rwMutexMaxReaders means a writer has
// the lock or is trying to get the lock. In the second case, readers should
// wait until the reader count becomes non-negative again to give the writer
// a chance to obtain the lock.
readers task.Futex
// waitingReaders are all of the tasks waiting for a read lock.
waitingReaders task.Stack
// Writer futex, normally 0. If there is a writer waiting until all readers
// have unlocked, this value is 1. It will be changed to a 2 (and get a
// wake) when the last reader unlocks.
writer task.Futex
// state is the current state of the RWMutex.
// Iff the mutex is completely unlocked, it contains rwMutexStateUnlocked (aka 0).
// Iff the mutex is write-locked, it contains rwMutexStateWLocked.
// While the mutex is read-locked, it contains the current number of readers.
state uint32
// Writer lock. Held between Lock() and Unlock().
writerLock Mutex
}
const (
rwMutexStateUnlocked = uint32(0)
rwMutexStateWLocked = ^uint32(0)
rwMutexMaxReaders = rwMutexStateWLocked - 1
)
const rwMutexMaxReaders = 1 << 30
func (rw *RWMutex) Lock() {
if rw.state == 0 {
// The mutex is completely unlocked.
// Lock without waiting.
rw.state = rwMutexStateWLocked
// Exclusive lock for writers.
rw.writerLock.Lock()
// Flag that we need to be awakened after the last read-lock unlocks.
rw.writer.Store(1)
// Signal to readers that they can't lock this mutex anymore.
n := uint32(rwMutexMaxReaders)
waiting := rw.readers.Add(-n)
if int32(waiting) == -rwMutexMaxReaders {
// All readers were already unlocked, so we don't need to wait for them.
rw.writer.Store(0)
return
}
// Wait for the lock to be released.
rw.waitingWriters.Push(task.Current())
task.Pause()
// There is at least one reader.
// Wait until all readers are unlocked. The last reader to unlock will set
// rw.writer to 2 and awaken us.
for rw.writer.Load() == 1 {
rw.writer.Wait(1)
}
rw.writer.Store(0)
}
func (rw *RWMutex) Unlock() {
switch rw.state {
case rwMutexStateWLocked:
// This is correct.
case rwMutexStateUnlocked:
// The mutex is already unlocked.
panic("sync: unlock of unlocked RWMutex")
default:
// The mutex is read-locked instead of write-locked.
panic("sync: write-unlock of read-locked RWMutex")
// Signal that new readers can lock this mutex.
waiting := rw.readers.Add(rwMutexMaxReaders)
if waiting != 0 {
// Awaken all waiting readers.
rw.readers.WakeAll()
}
switch {
case rw.maybeUnblockReaders():
// Switched over to read mode.
case rw.maybeUnblockWriter():
// Transferred to another writer.
default:
// Nothing is waiting for the lock.
rw.state = rwMutexStateUnlocked
}
// Done with this lock (next writer can try to get a lock).
rw.writerLock.Unlock()
}
func (rw *RWMutex) RLock() {
if rw.state == rwMutexStateWLocked {
// Wait for the write lock to be released.
rw.waitingReaders.Push(task.Current())
task.Pause()
return
}
// Add us as a reader.
newVal := rw.readers.Add(1)
if rw.state == rwMutexMaxReaders {
panic("sync: too many readers on RWMutex")
// Wait until the RWMutex is available for readers.
for int32(newVal) <= 0 {
rw.readers.Wait(newVal)
newVal = rw.readers.Load()
}
// Increase the reader count.
rw.state++
}
func (rw *RWMutex) RUnlock() {
switch rw.state {
case rwMutexStateUnlocked:
// The mutex is already unlocked.
panic("sync: unlock of unlocked RWMutex")
// Remove us as a reader.
one := uint32(1)
readers := int32(rw.readers.Add(-one))
case rwMutexStateWLocked:
// The mutex is write-locked instead of read-locked.
panic("sync: read-unlock of write-locked RWMutex")
// Check whether RUnlock was called too often.
if readers == -1 || readers == (-rwMutexMaxReaders)-1 {
runtimePanic("sync: RUnlock of unlocked RWMutex")
}
rw.state--
if rw.state == rwMutexStateUnlocked {
// This was the last reader.
// Try to unblock a writer.
rw.maybeUnblockWriter()
}
}
func (rw *RWMutex) maybeUnblockReaders() bool {
var n uint32
for {
t := rw.waitingReaders.Pop()
if t == nil {
break
if readers == -rwMutexMaxReaders {
// This was the last read lock. Check whether we need to wake up a write
// lock.
if rw.writer.CompareAndSwap(1, 2) {
rw.writer.Wake()
}
n++
scheduleTask(t)
}
if n == 0 {
return false
}
rw.state = n
return true
}
func (rw *RWMutex) maybeUnblockWriter() bool {
t := rw.waitingWriters.Pop()
if t == nil {
return false
}
rw.state = rwMutexStateWLocked
scheduleTask(t)
return true
}
type Locker interface {
+9
View File
@@ -32,6 +32,15 @@ SECTIONS
_stack_top = .;
} >RAM
/* Stack for second core (core 1), if there is one. This memory area won't be
* reserved if there is no second core. */
.stack1 (NOLOAD) :
{
. = ALIGN(4);
. += _stack_size;
_stack1_top = .;
} >RAM
/* Start address (in flash) of .data, used by startup code. */
_sidata = LOADADDR(.data);
+1
View File
@@ -12,6 +12,7 @@ var wg sync.WaitGroup
type intchan chan int
func main() {
time.Sleep(time.Second * 2)
ch := make(chan int, 2)
ch <- 1
println("len, cap of channel:", len(ch), cap(ch), ch == nil)
+15 -5
View File
@@ -5,19 +5,27 @@ import (
"time"
)
func init() {
println("init")
go println("goroutine in init")
time.Sleep(1 * time.Millisecond)
}
//func init() {
// println("\n----")
// println("init")
// go println("goroutine in init")
// time.Sleep(1 * time.Millisecond)
//}
func main() {
//for i := 0; i < 2; i++ {
// println("...")
// time.Sleep(time.Second)
//}
println("main 1")
go sub()
time.Sleep(1 * time.Millisecond)
println("main 2")
time.Sleep(2 * time.Millisecond)
println("main 3")
//time.Sleep(2 * time.Millisecond)
//println("main 4")
// Await a blocking call.
println("wait:")
@@ -101,6 +109,8 @@ func sub() {
println("sub 1")
time.Sleep(2 * time.Millisecond)
println("sub 2")
//time.Sleep(2 * time.Millisecond)
//println("sub 3")
}
func wait() {
@@ -26,7 +26,6 @@ package runtime
import (
_ "unsafe"
"runtime/interrupt"
)
// Documentation:
@@ -41,29 +40,29 @@ import (
func __atomic_load_{{.}}(ptr *uint{{$bits}}, ordering uintptr) uint{{$bits}} {
// The LLVM docs for this say that there is a val argument after the pointer.
// That is a typo, and the GCC docs omit it.
mask := interrupt.Disable()
mask := atomicLock()
val := *ptr
interrupt.Restore(mask)
atomicUnlock(mask)
return val
}
{{end}}
{{- define "store"}}{{$bits := mul . 8 -}}
//export __atomic_store_{{.}}
func __atomic_store_{{.}}(ptr *uint{{$bits}}, val uint{{$bits}}, ordering uintptr) {
mask := interrupt.Disable()
mask := atomicLock()
*ptr = val
interrupt.Restore(mask)
atomicUnlock(mask)
}
{{end}}
{{- define "cas"}}{{$bits := mul . 8 -}}
//go:inline
func doAtomicCAS{{$bits}}(ptr *uint{{$bits}}, expected, desired uint{{$bits}}) uint{{$bits}} {
mask := interrupt.Disable()
mask := atomicLock()
old := *ptr
if old == expected {
*ptr = desired
}
interrupt.Restore(mask)
atomicUnlock(mask)
return old
}
@@ -82,10 +81,10 @@ func __atomic_compare_exchange_{{.}}(ptr, expected *uint{{$bits}}, desired uint{
{{- define "swap"}}{{$bits := mul . 8 -}}
//go:inline
func doAtomicSwap{{$bits}}(ptr *uint{{$bits}}, new uint{{$bits}}) uint{{$bits}} {
mask := interrupt.Disable()
mask := atomicLock()
old := *ptr
*ptr = new
interrupt.Restore(mask)
atomicUnlock(mask)
return old
}
@@ -111,11 +110,11 @@ func __atomic_exchange_{{.}}(ptr *uint{{$bits}}, new uint{{$bits}}, ordering uin
//go:inline
func {{$opfn}}(ptr *{{$type}}, value {{$type}}) (old, new {{$type}}) {
mask := interrupt.Disable()
mask := atomicLock()
old = *ptr
{{$opdef}}
*ptr = new
interrupt.Restore(mask)
atomicUnlock(mask)
return old, new
}