Skip to content

Commit 0ca0db4

Browse files
committed
WIP map every goroutine to a new OS thread
1 parent e6f61b0 commit 0ca0db4

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"}
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`)
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
@@ -247,7 +247,6 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
247247
GOARCH: options.GOARCH,
248248
BuildTags: []string{options.GOOS, options.GOARCH},
249249
GC: "precise",
250-
Scheduler: "tasks",
251250
Linker: "cc",
252251
DefaultStackSize: 1024 * 64, // 64kB
253252
GDB: []string{"gdb"},
@@ -378,6 +377,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
378377
platformVersion = "11.0.0" // first macosx platform with arm64 support
379378
}
380379
llvmvendor = "apple"
380+
spec.Scheduler = "tasks"
381381
spec.Linker = "ld.lld"
382382
spec.Libc = "darwin-libSystem"
383383
// Use macosx* instead of darwin, otherwise darwin/arm64 will refer to
@@ -395,6 +395,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
395395
"src/runtime/runtime_unix.c",
396396
"src/runtime/signal.c")
397397
case "linux":
398+
spec.Scheduler = "threads"
398399
spec.Linker = "ld.lld"
399400
spec.RTLib = "compiler-rt"
400401
spec.Libc = "musl"
@@ -415,9 +416,11 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
415416
}
416417
spec.ExtraFiles = append(spec.ExtraFiles,
417418
"src/internal/futex/futex_linux.c",
419+
"src/internal/task/task_threads.c",
418420
"src/runtime/runtime_unix.c",
419421
"src/runtime/signal.c")
420422
case "windows":
423+
spec.Scheduler = "tasks"
421424
spec.Linker = "ld.lld"
422425
spec.Libc = "mingw-w64"
423426
// 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 crated 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)