Skip to content

Commit

Permalink
runtime: map every goroutine to a new OS thread
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
aykevl committed Dec 6, 2024
1 parent a3b9be7 commit 6894f62
Show file tree
Hide file tree
Showing 11 changed files with 567 additions and 4 deletions.
2 changes: 1 addition & 1 deletion compileopts/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
validSerialOptions = []string{"none", "uart", "usb", "rtt"}
validPrintSizeOptions = []string{"none", "short", "full"}
validPanicStrategyOptions = []string{"print", "trap"}
Expand Down
2 changes: 1 addition & 1 deletion compileopts/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
expectedPanicStrategyError := errors.New(`invalid panic option 'incorrect': valid values are print, trap`)

Expand Down
5 changes: 4 additions & 1 deletion compileopts/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,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"},
Expand Down Expand Up @@ -378,6 +377,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
Expand All @@ -395,6 +395,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"
Expand All @@ -415,9 +416,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
Expand Down
9 changes: 9 additions & 0 deletions src/internal/task/linux.go
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions src/internal/task/semaphore.go
Original file line number Diff line number Diff line change
@@ -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()
}
}
104 changes: 104 additions & 0 deletions src/internal/task/task_threads.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//go:build none

#define _GNU_SOURCE
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.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, 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);
}

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 crated 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);
}
Loading

0 comments on commit 6894f62

Please sign in to comment.