Skip to content

Commit 75f8cf0

Browse files
committed
internal/llsync: implement primitives for preemptive scheduling
1 parent 4e6078f commit 75f8cf0

File tree

8 files changed

+138
-0
lines changed

8 files changed

+138
-0
lines changed

src/internal/llsync/atomic-cooperative.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 llsync
24

35
// Atomics implementation for cooperative systems. The atomic types here aren't
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//go:build scheduler.threads
2+
3+
package llsync
4+
5+
// Atomics implementation for non-cooperative systems (multithreaded, etc).
6+
// These atomic types use real atomic instructions.
7+
8+
import "sync/atomic"
9+
10+
type (
11+
Uintptr = atomic.Uintptr
12+
Uint32 = atomic.Uint32
13+
Uint64 = atomic.Uint64
14+
)

src/internal/llsync/futex-cooperative.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 llsync
24

35
import (
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//go:build scheduler.threads
2+
3+
package llsync
4+
5+
// A futex is a way for userspace to wait with the pointer as the key, and for
6+
// another thread to wake one or all waiting threads keyed on the same pointer.
7+
//
8+
// A futex does not change the underlying value, it only reads it before to prevent
9+
// lost wake-ups.
10+
type Futex struct {
11+
Uint32
12+
}

src/internal/llsync/futex_linux.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//go:build none
2+
3+
// This file is manually included, to avoid CGo which would cause a circular
4+
// import.
5+
6+
#include <stdint.h>
7+
#include <sys/syscall.h>
8+
#include <unistd.h>
9+
10+
#define FUTEX_WAIT 0
11+
#define FUTEX_WAKE 1
12+
#define FUTEX_PRIVATE 128
13+
14+
void tinygo_futex_wait(uint32_t *addr, uint32_t cmp) {
15+
syscall(SYS_futex, addr, FUTEX_WAIT|FUTEX_PRIVATE, cmp, NULL, NULL, 0);
16+
}
17+
18+
void tinygo_futex_wake(uint32_t *addr, uint32_t num) {
19+
syscall(SYS_futex, addr, FUTEX_WAKE|FUTEX_PRIVATE, num, NULL, NULL, 0);
20+
}

src/internal/llsync/futex_linux.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//go:build scheduler.threads
2+
3+
package llsync
4+
5+
import (
6+
"unsafe"
7+
)
8+
9+
// Atomically check for cmp to still be equal to the futex value and if so, go
10+
// to sleep. Return true if we were definitely awoken by a call to Wake or
11+
// WakeAll, and false if we can't be sure of that.
12+
func (f *Futex) Wait(cmp uint32) bool {
13+
tinygo_futex_wait((*uint32)(unsafe.Pointer(&f.Uint32)), cmp)
14+
15+
// We *could* detect a zero return value from the futex system call which
16+
// would indicate we got awoken by a Wake or WakeAll call. However, this is
17+
// what the manual page has to say:
18+
//
19+
// > Note that a wake-up can also be caused by common futex usage patterns
20+
// > in unrelated code that happened to have previously used the futex
21+
// > word's memory location (e.g., typical futex-based implementations of
22+
// > Pthreads mutexes can cause this under some conditions). Therefore,
23+
// > callers should always conservatively assume that a return value of 0
24+
// > can mean a spurious wake-up, and use the futex word's value (i.e., the
25+
// > user-space synchronization scheme) to decide whether to continue to
26+
// > block or not.
27+
//
28+
// I'm not sure whether we do anything like pthread does, so to be on the
29+
// safe side we say we don't know whether the wakeup was spurious or not and
30+
// return false.
31+
return false
32+
}
33+
34+
// Wake a single waiter.
35+
func (f *Futex) Wake() {
36+
tinygo_futex_wake((*uint32)(unsafe.Pointer(&f.Uint32)), 1)
37+
}
38+
39+
// Wake all waiters.
40+
func (f *Futex) WakeAll() {
41+
const maxInt32 = 0x7fff_ffff
42+
tinygo_futex_wake((*uint32)(unsafe.Pointer(&f.Uint32)), maxInt32)
43+
}
44+
45+
//export tinygo_futex_wait
46+
func tinygo_futex_wait(addr *uint32, cmp uint32)
47+
48+
//export tinygo_futex_wake
49+
func tinygo_futex_wake(addr *uint32, num uint32)

src/internal/llsync/plock-cooperative.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 llsync
24

35
// PMutex is a real mutex on systems that can be either preemptive or threaded,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//go:build scheduler.threads
2+
3+
package llsync
4+
5+
// PMutex is a real mutex on systems that can be either preemptive or threaded,
6+
// and a dummy lock on other (purely cooperative) systems.
7+
//
8+
// It is mainly useful for short operations that need a lock when threading may
9+
// be involved, but which do not need a lock with a purely cooperative
10+
// scheduler.
11+
type PMutex struct {
12+
futex Futex
13+
}
14+
15+
func (m *PMutex) Lock() {
16+
// Fast path: try to take an uncontended lock.
17+
if m.futex.CompareAndSwap(0, 1) {
18+
// We obtained the mutex.
19+
return
20+
}
21+
22+
// Try to lock the mutex. If it changed from 0 to 2, we took a contended
23+
// lock.
24+
for m.futex.Swap(2) != 0 {
25+
// Wait until we get resumed in Unlock.
26+
m.futex.Wait(2)
27+
}
28+
}
29+
30+
func (m *PMutex) Unlock() {
31+
if old := m.futex.Swap(0); old == 2 {
32+
// Mutex was a contended lock, so we need to wake the next waiter.
33+
m.futex.Wake()
34+
}
35+
// Note: this implementation doesn't check for an unlock of an unlocked
36+
// mutex to keep it as lightweight as possible.
37+
}

0 commit comments

Comments
 (0)