Skip to content

Commit d328dd4

Browse files
committed
WIP map every goroutine to a new OS thread
1 parent 6ae73cb commit d328dd4

16 files changed

+1111
-4
lines changed

compileopts/config.go

+1
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ func (c *Config) CFlags(libclang bool) []string {
340340
"-nostdlibinc",
341341
"-isystem", filepath.Join(path, "include"),
342342
"-isystem", filepath.Join(root, "lib", "musl", "arch", arch),
343+
"-isystem", filepath.Join(root, "lib", "musl", "arch", "generic"),
343344
"-isystem", filepath.Join(root, "lib", "musl", "include"),
344345
)
345346
case "wasi-libc":

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
@@ -394,6 +394,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
394394
"src/runtime/runtime_unix.c",
395395
"src/runtime/signal.c")
396396
case "linux":
397+
spec.Scheduler = "threads"
397398
spec.Linker = "ld.lld"
398399
spec.RTLib = "compiler-rt"
399400
spec.Libc = "musl"
@@ -414,9 +415,11 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
414415
}
415416
spec.ExtraFiles = append(spec.ExtraFiles,
416417
"src/internal/task/futex_linux.c",
418+
"src/internal/task/task_threads.c",
417419
"src/runtime/runtime_unix.c",
418420
"src/runtime/signal.c")
419421
case "windows":
422+
spec.Scheduler = "tasks"
420423
spec.Linker = "ld.lld"
421424
spec.Libc = "mingw-w64"
422425
// Note: using a medium code model, low image base and no ASLR

src/internal/task/futex_linux.c

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#include <stdint.h>
77
#include <sys/syscall.h>
8+
#include <time.h>
89
#include <unistd.h>
910

1011
#define FUTEX_WAIT 0
@@ -15,6 +16,13 @@ void tinygo_futex_wait(uint32_t *addr, uint32_t cmp) {
1516
syscall(SYS_futex, addr, FUTEX_WAIT|FUTEX_PRIVATE, cmp, NULL, NULL, 0);
1617
}
1718

19+
void tinygo_futex_wait_timeout(uint32_t *addr, uint32_t cmp, uint64_t timeout) {
20+
struct timespec ts = {0};
21+
ts.tv_sec = timeout / 1000000000;
22+
ts.tv_nsec = timeout % 1000000000;
23+
syscall(SYS_futex, addr, FUTEX_WAIT|FUTEX_PRIVATE, cmp, &ts, NULL, 0);
24+
}
25+
1826
void tinygo_futex_wake(uint32_t *addr, uint32_t num) {
1927
syscall(SYS_futex, addr, FUTEX_WAKE|FUTEX_PRIVATE, num, NULL, NULL, 0);
2028
}

src/internal/task/futex_linux.go

+8
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ func (f *Futex) Wait(cmp uint32) bool {
3131
return false
3232
}
3333

34+
// Like Wait, but times out after the number of nanoseconds in timeout.
35+
func (f *Futex) WaitUntil(cmp uint32, timeout uint64) {
36+
tinygo_futex_wait_timeout((*uint32)(unsafe.Pointer(&f.Uint32)), cmp, timeout)
37+
}
38+
3439
// Wake a single waiter.
3540
func (f *Futex) Wake() {
3641
tinygo_futex_wake((*uint32)(unsafe.Pointer(&f.Uint32)), 1)
@@ -45,5 +50,8 @@ func (f *Futex) WakeAll() {
4550
//export tinygo_futex_wait
4651
func tinygo_futex_wait(addr *uint32, cmp uint32)
4752

53+
//export tinygo_futex_wait_timeout
54+
func tinygo_futex_wait_timeout(addr *uint32, cmp uint32, timeout uint64)
55+
4856
//export tinygo_futex_wake
4957
func tinygo_futex_wake(addr *uint32, num uint32)

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.go

+17
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,23 @@ type Task struct {
2626
DeferFrame unsafe.Pointer
2727
}
2828

29+
// DataUint32 returns the Data field as a uint32. The value is only valid after
30+
// setting it through SetDataUint32 or by storing to it using DataAtomicUint32.
31+
func (t *Task) DataUint32() uint32 {
32+
return *(*uint32)(unsafe.Pointer(&t.Data))
33+
}
34+
35+
// SetDataUint32 updates the uint32 portion of the Data field (which could be
36+
// the first 4 or last 4 bytes depending on the architecture endianness).
37+
func (t *Task) SetDataUint32(value uint32) {
38+
*(*uint32)(unsafe.Pointer(&t.Data)) = value
39+
}
40+
41+
// DataAtomicUint32 returns the Data field as an atomic-if-needed Uint32 value.
42+
func (t *Task) DataAtomicUint32() *Uint32 {
43+
return (*Uint32)(unsafe.Pointer(&t.Data))
44+
}
45+
2946
// getGoroutineStackSize is a compiler intrinsic that returns the stack size for
3047
// the given function and falls back to the default stack size. It is replaced
3148
// with a load from a special section just before codegen.

src/internal/task/task_threads.c

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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+
sem_t startlock;
25+
};
26+
27+
// Handle the GC pause in Go.
28+
void tinygo_task_gc_pause(int sig);
29+
30+
// Initialize the main thread.
31+
void tinygo_task_init(void *mainTask, pthread_t *thread, void *context) {
32+
// Make sure the current task pointer is set correctly for the main
33+
// goroutine as well.
34+
current_task = mainTask;
35+
36+
// Store the thread ID of the main thread.
37+
*thread = pthread_self();
38+
39+
// Register the "GC pause" signal for the entire process.
40+
// Using pthread_kill, we can still send the signal to a specific thread.
41+
struct sigaction act = { 0 };
42+
act.sa_flags = SA_SIGINFO;
43+
act.sa_handler = &tinygo_task_gc_pause;
44+
sigaction(taskPauseSignal, &act, NULL);
45+
}
46+
47+
void tinygo_task_exited(void*);
48+
49+
// Helper to start a goroutine while also storing the 'task' structure.
50+
static void* start_wrapper(void *arg) {
51+
struct state_pass *state = arg;
52+
void *(*start)(void*) = state->start;
53+
void *args = state->args;
54+
current_task = state->task;
55+
56+
// Notify the caller that the thread has successfully started and
57+
// initialized.
58+
sem_post(&state->startlock);
59+
60+
// Run the goroutine function.
61+
start(args);
62+
63+
// Notify the Go side this thread will exit.
64+
tinygo_task_exited(current_task);
65+
66+
return NULL;
67+
};
68+
69+
// Start a new goroutine in an OS thread.
70+
int tinygo_task_start(uintptr_t fn, void *args, void *task, pthread_t *thread, void *context) {
71+
// Sanity check. Should get optimized away.
72+
if (sizeof(pthread_t) != sizeof(void*)) {
73+
__builtin_trap();
74+
}
75+
76+
struct state_pass state = {
77+
.start = (void*)fn,
78+
.args = args,
79+
.task = task,
80+
};
81+
sem_init(&state.startlock, 0, 0);
82+
int result = pthread_create(thread, NULL, &start_wrapper, &state);
83+
84+
// Wait until the thread has been crated and read all state_pass variables.
85+
sem_wait(&state.startlock);
86+
87+
return result;
88+
}
89+
90+
// Return the current task (for task.Current()).
91+
void* tinygo_task_current(void) {
92+
return current_task;
93+
}
94+
95+
// Obtain the highest address of the stack.
96+
uintptr_t tinygo_task_stacktop(void) {
97+
pthread_attr_t attr;
98+
pthread_getattr_np(pthread_self(), &attr);
99+
void *stackbase;
100+
size_t stacksize;
101+
pthread_attr_getstack(&attr, &stackbase, &stacksize);
102+
pthread_attr_destroy(&attr);
103+
return (uintptr_t)stackbase + (uintptr_t)stacksize;
104+
}
105+
106+
// Send a signal to cause the task to pause for the GC mark phase.
107+
void tinygo_task_send_gc_signal(pthread_t thread) {
108+
pthread_kill(thread, taskPauseSignal);
109+
}

0 commit comments

Comments
 (0)