Skip to content

Commit 5555c98

Browse files
committed
WIP map every goroutine to a new OS thread
1 parent 75f8cf0 commit 5555c98

File tree

11 files changed

+889
-2
lines changed

11 files changed

+889
-2
lines changed

compileopts/options.go

Lines changed: 1 addition & 1 deletion
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/target.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
413413
spec.CFlags = append(spec.CFlags, "-mno-outline-atomics")
414414
}
415415
spec.ExtraFiles = append(spec.ExtraFiles,
416+
"src/internal/task/task_threads.c",
416417
"src/runtime/runtime_unix.c",
417418
"src/runtime/signal.c")
418419
case "windows":

src/internal/llsync/semaphore.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package llsync
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/linux.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//go:build linux && !baremetal
2+
3+
package task
4+
5+
import "unsafe"
6+
7+
type pthread_mutex struct {
8+
// 40 bytes on a 64-bit system, 24 bytes on a 32-bit system
9+
state1 uint64
10+
state2 [4]uintptr
11+
}
12+
13+
// pthread_mutex_t and pthread_cond_t are both initialized to zero in
14+
// PTHREAD_*_INITIALIZER.
15+
16+
type sem struct {
17+
// 64 bytes on 64-bit systems, 32 bytes on 32-bit systems:
18+
// volatile int __val[4*sizeof(long)/sizeof(int)];
19+
state [4 * unsafe.Sizeof(uintptr(0)) / 4]int32
20+
}

src/internal/task/task_threads.c

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//go:build none
2+
3+
#include <pthread.h>
4+
#include <stdint.h>
5+
#include <stdio.h>
6+
#include <semaphore.h>
7+
8+
// Pointer to the current task.Task structure.
9+
// Ideally the entire task.Task structure would be a thread-local variable but
10+
// this also works.
11+
static __thread void *current_task;
12+
13+
struct state_pass {
14+
void *(*start)(void*);
15+
void *args;
16+
void *task;
17+
sem_t startlock;
18+
};
19+
20+
// Helper to start a goroutine while also storing the 'task' structure.
21+
static void* start_wrapper(void *arg) {
22+
struct state_pass *state = arg;
23+
void *(*start)(void*) = state->start;
24+
void *args = state->args;
25+
current_task = state->task;
26+
sem_post(&state->startlock);
27+
start(args);
28+
return NULL;
29+
};
30+
31+
// Start a new goroutine in an OS thread.
32+
int tinygo_task_start(uintptr_t fn, void *args, void *task, uint64_t id, void *context) {
33+
struct state_pass state = {
34+
.start = (void*)fn,
35+
.args = args,
36+
.task = task,
37+
};
38+
sem_init(&state.startlock, 0, 0);
39+
pthread_t thread;
40+
int result = pthread_create(&thread, NULL, &start_wrapper, &state);
41+
42+
// Wait until the thread has been crated and read all state_pass variables.
43+
sem_wait(&state.startlock);
44+
45+
return result;
46+
}
47+
48+
// Return the current task (for task.Current()).
49+
void* tinygo_task_current(void) {
50+
return current_task;
51+
}
52+
53+
// Set the current task at startup.
54+
void tinygo_task_set_current(void *task, void *context) {
55+
current_task = task;
56+
}
57+
58+
uintptr_t tinygo_mutex_size(void) {
59+
return sizeof(pthread_mutex_t);
60+
}
61+
62+
uintptr_t tinygo_mutex_align(void) {
63+
return _Alignof(pthread_mutex_t);
64+
}
65+
66+
uintptr_t tinygo_sem_size(void) {
67+
return sizeof(sem_t);
68+
}
69+
70+
uintptr_t tinygo_sem_align(void) {
71+
return _Alignof(sem_t);
72+
}

src/internal/task/task_threads.go

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
//go:build scheduler.threads
2+
3+
package task
4+
5+
import (
6+
"sync/atomic"
7+
"unsafe"
8+
)
9+
10+
// If true, print verbose debug logs.
11+
const verbose = false
12+
13+
// Scheduler-specific state.
14+
type state struct {
15+
// Goroutine ID. The number here is not really significant and after a while
16+
// it could wrap around. But it is useful for debugging.
17+
id uint64
18+
19+
// Semaphore to pause/resume the thread atomically.
20+
sem sem
21+
}
22+
23+
// Goroutine counter, starting at 0 for the main goroutine.
24+
var goroutineID uint64
25+
26+
var mainTask Task
27+
28+
func OnSystemStack() bool {
29+
runtimePanic("todo: task.OnSystemStack")
30+
return false
31+
}
32+
33+
// Initialize the main goroutine state. Must be called by the runtime on
34+
// startup, before starting any other goroutines.
35+
func Init() {
36+
// Sanity check. With ThinLTO, this should be getting optimized away.
37+
if unsafe.Sizeof(pthread_mutex{}) != tinygo_mutex_size() {
38+
panic("internal/task: unexpected sizeof(pthread_mutex_t)")
39+
}
40+
if unsafe.Alignof(pthread_mutex{}) != tinygo_mutex_align() {
41+
panic("internal/task: unexpected _Alignof(pthread_mutex_t)")
42+
}
43+
if unsafe.Sizeof(sem{}) != tinygo_sem_size() {
44+
panic("semaphore is an unexpected size!")
45+
}
46+
if unsafe.Alignof(sem{}) != tinygo_sem_align() {
47+
panic("semaphore is an unexpected alignment!")
48+
}
49+
50+
mainTask.init()
51+
tinygo_task_set_current(&mainTask)
52+
}
53+
54+
func (t *Task) init() {
55+
sem_init(&t.state.sem, 0, 0)
56+
}
57+
58+
// Return the task struct for the current thread.
59+
func Current() *Task {
60+
t := (*Task)(tinygo_task_current())
61+
if t == nil {
62+
runtimePanic("unknown current task")
63+
}
64+
return t
65+
}
66+
67+
// Pause pauses the current task, until it is resumed by another task.
68+
// It is possible that another task has called Resume() on the task before it
69+
// hits Pause(), in which case the task won't be paused but continues
70+
// immediately.
71+
func Pause() {
72+
// Wait until resumed
73+
t := Current()
74+
if verbose {
75+
println("*** pause: ", t.state.id)
76+
}
77+
if sem_wait(&t.state.sem) != 0 {
78+
runtimePanic("sem_wait error!")
79+
}
80+
}
81+
82+
// Resume the given task.
83+
// It is legal to resume a task before it gets paused, it means that the next
84+
// call to Pause() won't pause but will continue immediately. This happens in
85+
// practice sometimes in channel operations, where the Resume() might get called
86+
// between the channel unlock and the call to Pause().
87+
func (t *Task) Resume() {
88+
if verbose {
89+
println("*** resume: ", t.state.id)
90+
}
91+
// Increment the semaphore counter.
92+
// If the task is currently paused in sem_wait, it will resume.
93+
// If the task is not yet paused, the next call to sem_wait will continue
94+
// immediately.
95+
if sem_post(&t.state.sem) != 0 {
96+
runtimePanic("sem_post: error!")
97+
}
98+
}
99+
100+
// Start a new OS thread.
101+
func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
102+
t := &Task{}
103+
t.state.id = atomic.AddUint64(&goroutineID, 1)
104+
if verbose {
105+
println("*** start: ", t.state.id, "from", Current().state.id)
106+
}
107+
t.init()
108+
errCode := tinygo_task_start(fn, args, t, t.state.id)
109+
if errCode != 0 {
110+
runtimePanic("could not start thread")
111+
}
112+
}
113+
114+
type AsyncLock struct {
115+
// TODO: lock on macOS needs to be initialized with a magic value
116+
pthread_mutex
117+
}
118+
119+
func (l *pthread_mutex) Lock() {
120+
errCode := pthread_mutex_lock(l)
121+
if errCode != 0 {
122+
runtimePanic("mutex Lock has error code")
123+
}
124+
}
125+
126+
func (l *pthread_mutex) TryLock() bool {
127+
return pthread_mutex_trylock(l) == 0
128+
}
129+
130+
func (l *pthread_mutex) Unlock() {
131+
errCode := pthread_mutex_unlock(l)
132+
if errCode != 0 {
133+
runtimePanic("mutex Unlock has error code")
134+
}
135+
}
136+
137+
//go:linkname runtimePanic runtime.runtimePanic
138+
func runtimePanic(msg string)
139+
140+
// Using //go:linkname instead of //export so that we don't tell the compiler
141+
// that the 't' parameter won't escape (because it will).
142+
//
143+
//go:linkname tinygo_task_set_current tinygo_task_set_current
144+
func tinygo_task_set_current(t *Task)
145+
146+
// Here same as for tinygo_task_set_current.
147+
//
148+
//go:linkname tinygo_task_start tinygo_task_start
149+
func tinygo_task_start(fn uintptr, args unsafe.Pointer, t *Task, id uint64) int32
150+
151+
//export tinygo_task_current
152+
func tinygo_task_current() unsafe.Pointer
153+
154+
//export tinygo_mutex_size
155+
func tinygo_mutex_size() uintptr
156+
157+
//export tinygo_mutex_align
158+
func tinygo_mutex_align() uintptr
159+
160+
//export pthread_mutex_lock
161+
func pthread_mutex_lock(*pthread_mutex) int32
162+
163+
//export pthread_mutex_trylock
164+
func pthread_mutex_trylock(*pthread_mutex) int32
165+
166+
//export pthread_mutex_unlock
167+
func pthread_mutex_unlock(*pthread_mutex) int32
168+
169+
//export sem_init
170+
func sem_init(s *sem, pshared int32, value uint32) int32
171+
172+
//export sem_wait
173+
func sem_wait(*sem) int32
174+
175+
//export sem_post
176+
func sem_post(*sem) int32
177+
178+
//export tinygo_sem_size
179+
func tinygo_sem_size() uintptr
180+
181+
//export tinygo_sem_align
182+
func tinygo_sem_align() uintptr

src/runtime/chan.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build !scheduler.threads
2+
13
package runtime
24

35
// This file implements the 'chan' type and send/receive/select operations.

0 commit comments

Comments
 (0)