Skip to content

Commit 3767dec

Browse files
committed
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.
1 parent 8d6e160 commit 3767dec

11 files changed

+567
-4
lines changed

compileopts/options.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
var (
1111
validBuildModeOptions = []string{"default", "c-shared"}
1212
validGCOptions = []string{"none", "leaking", "conservative", "custom", "precise"}
13-
validSchedulerOptions = []string{"none", "tasks", "asyncify"}
13+
validSchedulerOptions = []string{"none", "tasks", "asyncify", "threads"}
1414
validSerialOptions = []string{"none", "uart", "usb", "rtt"}
1515
validPrintSizeOptions = []string{"none", "short", "full", "html"}
1616
validPanicStrategyOptions = []string{"print", "trap"}

compileopts/options_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
func TestVerifyOptions(t *testing.T) {
1111

1212
expectedGCError := errors.New(`invalid gc option 'incorrect': valid values are none, leaking, conservative, custom, precise`)
13-
expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, asyncify`)
13+
expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, asyncify, threads`)
1414
expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full, html`)
1515
expectedPanicStrategyError := errors.New(`invalid panic option 'incorrect': valid values are print, trap`)
1616

compileopts/target.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,6 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
248248
GOARCH: options.GOARCH,
249249
BuildTags: []string{options.GOOS, options.GOARCH},
250250
GC: "precise",
251-
Scheduler: "tasks",
252251
Linker: "cc",
253252
DefaultStackSize: 1024 * 64, // 64kB
254253
GDB: []string{"gdb"},
@@ -381,6 +380,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
381380
platformVersion = "11.0.0" // first macosx platform with arm64 support
382381
}
383382
llvmvendor = "apple"
383+
spec.Scheduler = "tasks"
384384
spec.Linker = "ld.lld"
385385
spec.Libc = "darwin-libSystem"
386386
// Use macosx* instead of darwin, otherwise darwin/arm64 will refer to
@@ -398,6 +398,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
398398
"src/runtime/runtime_unix.c",
399399
"src/runtime/signal.c")
400400
case "linux":
401+
spec.Scheduler = "threads"
401402
spec.Linker = "ld.lld"
402403
spec.RTLib = "compiler-rt"
403404
spec.Libc = "musl"
@@ -418,9 +419,11 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
418419
}
419420
spec.ExtraFiles = append(spec.ExtraFiles,
420421
"src/internal/futex/futex_linux.c",
422+
"src/internal/task/task_threads.c",
421423
"src/runtime/runtime_unix.c",
422424
"src/runtime/signal.c")
423425
case "windows":
426+
spec.Scheduler = "tasks"
424427
spec.Linker = "ld.lld"
425428
spec.Libc = "mingw-w64"
426429
// Note: using a medium code model, low image base and no ASLR

src/internal/task/linux.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//go:build linux && !baremetal
2+
3+
package task
4+
5+
import "unsafe"
6+
7+
// Musl uses a pointer (or unsigned long for C++) so unsafe.Pointer should be
8+
// fine.
9+
type threadID unsafe.Pointer

src/internal/task/semaphore.go

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package task
2+
3+
// Barebones semaphore implementation.
4+
// The main limitation is that if there are multiple waiters, a single Post()
5+
// call won't do anything. Only when Post() has been called to awaken all
6+
// waiters will the waiters proceed.
7+
// This limitation is not a problem when there will only be a single waiter.
8+
type Semaphore struct {
9+
futex Futex
10+
}
11+
12+
// Post (unlock) the semaphore, incrementing the value in the semaphore.
13+
func (s *Semaphore) Post() {
14+
newValue := s.futex.Add(1)
15+
if newValue == 0 {
16+
s.futex.WakeAll()
17+
}
18+
}
19+
20+
// Wait (lock) the semaphore, decrementing the value in the semaphore.
21+
func (s *Semaphore) Wait() {
22+
delta := int32(-1)
23+
value := s.futex.Add(uint32(delta))
24+
for {
25+
if int32(value) >= 0 {
26+
// Semaphore unlocked!
27+
return
28+
}
29+
s.futex.Wait(value)
30+
value = s.futex.Load()
31+
}
32+
}

src/internal/task/task_threads.c

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//go:build none
2+
3+
#define _GNU_SOURCE
4+
#include <pthread.h>
5+
#include <semaphore.h>
6+
#include <signal.h>
7+
#include <stdint.h>
8+
#include <stdio.h>
9+
10+
// BDWGC also uses SIGRTMIN+6 on Linux, which seems like a reasonable choice.
11+
#ifdef __linux__
12+
#define taskPauseSignal (SIGRTMIN + 6)
13+
#endif
14+
15+
// Pointer to the current task.Task structure.
16+
// Ideally the entire task.Task structure would be a thread-local variable but
17+
// this also works.
18+
static __thread void *current_task;
19+
20+
struct state_pass {
21+
void *(*start)(void*);
22+
void *args;
23+
void *task;
24+
uintptr_t *stackTop;
25+
sem_t startlock;
26+
};
27+
28+
// Handle the GC pause in Go.
29+
void tinygo_task_gc_pause(int sig);
30+
31+
// Initialize the main thread.
32+
void tinygo_task_init(void *mainTask, pthread_t *thread, void *context) {
33+
// Make sure the current task pointer is set correctly for the main
34+
// goroutine as well.
35+
current_task = mainTask;
36+
37+
// Store the thread ID of the main thread.
38+
*thread = pthread_self();
39+
40+
// Register the "GC pause" signal for the entire process.
41+
// Using pthread_kill, we can still send the signal to a specific thread.
42+
struct sigaction act = { 0 };
43+
act.sa_flags = SA_SIGINFO;
44+
act.sa_handler = &tinygo_task_gc_pause;
45+
sigaction(taskPauseSignal, &act, NULL);
46+
}
47+
48+
void tinygo_task_exited(void*);
49+
50+
// Helper to start a goroutine while also storing the 'task' structure.
51+
static void* start_wrapper(void *arg) {
52+
struct state_pass *state = arg;
53+
void *(*start)(void*) = state->start;
54+
void *args = state->args;
55+
current_task = state->task;
56+
57+
// Save the current stack pointer in the goroutine state, for the GC.
58+
int stackAddr;
59+
*(state->stackTop) = (uintptr_t)(&stackAddr);
60+
61+
// Notify the caller that the thread has successfully started and
62+
// initialized.
63+
sem_post(&state->startlock);
64+
65+
// Run the goroutine function.
66+
start(args);
67+
68+
// Notify the Go side this thread will exit.
69+
tinygo_task_exited(current_task);
70+
71+
return NULL;
72+
};
73+
74+
// Start a new goroutine in an OS thread.
75+
int tinygo_task_start(uintptr_t fn, void *args, void *task, pthread_t *thread, uintptr_t *stackTop, void *context) {
76+
// Sanity check. Should get optimized away.
77+
if (sizeof(pthread_t) != sizeof(void*)) {
78+
__builtin_trap();
79+
}
80+
81+
struct state_pass state = {
82+
.start = (void*)fn,
83+
.args = args,
84+
.task = task,
85+
.stackTop = stackTop,
86+
};
87+
sem_init(&state.startlock, 0, 0);
88+
int result = pthread_create(thread, NULL, &start_wrapper, &state);
89+
90+
// Wait until the thread has been created and read all state_pass variables.
91+
sem_wait(&state.startlock);
92+
93+
return result;
94+
}
95+
96+
// Return the current task (for task.Current()).
97+
void* tinygo_task_current(void) {
98+
return current_task;
99+
}
100+
101+
// Send a signal to cause the task to pause for the GC mark phase.
102+
void tinygo_task_send_gc_signal(pthread_t thread) {
103+
pthread_kill(thread, taskPauseSignal);
104+
}

0 commit comments

Comments
 (0)