Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: threading support #4559

Draft
wants to merge 8 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 2 additions & 0 deletions src/internal/task/atomic-cooperative.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !scheduler.threads

package task

// Atomics implementation for cooperative systems. The atomic types here aren't
Expand Down
14 changes: 14 additions & 0 deletions src/internal/task/atomic-preemptive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build scheduler.threads

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 changes: 2 additions & 0 deletions src/internal/task/futex-cooperative.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !scheduler.threads

package task

// A futex is a way for userspace to wait with the pointer as the key, and for
Expand Down
7 changes: 7 additions & 0 deletions src/internal/task/futex-preemptive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build scheduler.threads

package task

import "internal/futex"

type Futex = futex.Futex
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
69 changes: 69 additions & 0 deletions src/internal/task/mutex.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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
// look. 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 changes: 2 additions & 0 deletions src/internal/task/pmutex-cooperative.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !scheduler.threads

package task

// PMutex is a real mutex on systems that can be either preemptive or threaded,
Expand Down
11 changes: 11 additions & 0 deletions src/internal/task/pmutex-preemptive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build scheduler.threads

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 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()
}
}
7 changes: 6 additions & 1 deletion src/internal/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type Task struct {
}

// DataUint32 returns the Data field as a uint32. The value is only valid after
// setting it through SetDataUint32.
// setting it through SetDataUint32 or by storing to it using DataAtomicUint32.
func (t *Task) DataUint32() uint32 {
return *(*uint32)(unsafe.Pointer(&t.Data))
}
Expand All @@ -38,6 +38,11 @@ func (t *Task) SetDataUint32(val uint32) {
*(*uint32)(unsafe.Pointer(&t.Data)) = val
}

// DataAtomicUint32 returns the Data field as an atomic-if-needed Uint32 value.
func (t *Task) DataAtomicUint32() *Uint32 {
return (*Uint32)(unsafe.Pointer(&t.Data))
}

// getGoroutineStackSize is a compiler intrinsic that returns the stack size for
// the given function and falls back to the default stack size. It is replaced
// with a load from a special section just before codegen.
Expand Down
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
Loading