Skip to content

Commit 5b243f6

Browse files
aykevldeadprogram
authored andcommitted
internal/task: implement atomic primitives for preemptive scheduling
1 parent 789b5c6 commit 5b243f6

File tree

8 files changed

+111
-0
lines changed

8 files changed

+111
-0
lines changed

src/internal/task/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 task
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 task
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/task/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 task
24

35
// A futex is a way for userspace to wait with the pointer as the key, and for

src/internal/task/futex-preemptive.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//go:build scheduler.threads
2+
3+
package task
4+
5+
import "internal/futex"
6+
7+
type Futex = futex.Futex

src/internal/task/mutex-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 task
24

35
type Mutex struct {

src/internal/task/mutex-preemptive.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//go:build scheduler.threads
2+
3+
package task
4+
5+
// Futex-based mutex.
6+
// This is largely based on the paper "Futexes are Tricky" by Ulrich Drepper.
7+
// It describes a few ways to implement mutexes using a futex, and how some
8+
// seemingly-obvious implementations don't exactly work as intended.
9+
// Unfortunately, Go atomic operations work slightly differently so we can't
10+
// copy the algorithm verbatim.
11+
//
12+
// The implementation works like this. The futex can have 3 different values,
13+
// depending on the state:
14+
//
15+
// - 0: the futex is currently unlocked.
16+
// - 1: the futex is locked, but is uncontended. There is one special case: if
17+
// a contended futex is unlocked, it is set to 0. It is possible for another
18+
// thread to lock the futex before the next waiter is woken. But because a
19+
// waiter will be woken (if there is one), it will always change to 2
20+
// regardless. So this is not a problem.
21+
// - 2: the futex is locked, and is contended. At least one thread is trying
22+
// to obtain the lock (and is in the contended loop, see below).
23+
//
24+
// For the paper, see:
25+
// https://dept-info.labri.fr/~denis/Enseignement/2008-IR/Articles/01-futex.pdf)
26+
27+
type Mutex struct {
28+
futex Futex
29+
}
30+
31+
func (m *Mutex) Lock() {
32+
// Fast path: try to take an uncontended lock.
33+
if m.futex.CompareAndSwap(0, 1) {
34+
// We obtained the mutex.
35+
return
36+
}
37+
38+
// The futex is contended, so we enter the contended loop.
39+
// If we manage to change the futex from 0 to 2, we managed to take the
40+
// lock. Else, we have to wait until a call to Unlock unlocks this mutex.
41+
// (Unlock will wake one waiter when it finds the futex is set to 2 when
42+
// unlocking).
43+
for m.futex.Swap(2) != 0 {
44+
// Wait until we get resumed in Unlock.
45+
m.futex.Wait(2)
46+
}
47+
}
48+
49+
func (m *Mutex) Unlock() {
50+
if old := m.futex.Swap(0); old == 0 {
51+
// Mutex wasn't locked before.
52+
panic("sync: unlock of unlocked Mutex")
53+
} else if old == 2 {
54+
// Mutex was a contended lock, so we need to wake the next waiter.
55+
m.futex.Wake()
56+
}
57+
}
58+
59+
// TryLock tries to lock m and reports whether it succeeded.
60+
//
61+
// Note that while correct uses of TryLock do exist, they are rare,
62+
// and use of TryLock is often a sign of a deeper problem
63+
// in a particular use of mutexes.
64+
func (m *Mutex) TryLock() bool {
65+
// Fast path: try to take an uncontended lock.
66+
if m.futex.CompareAndSwap(0, 1) {
67+
// We obtained the mutex.
68+
return true
69+
}
70+
return false
71+
}

src/internal/task/pmutex-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 task
24

35
// PMutex is a real mutex on systems that can be either preemptive or threaded,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//go:build scheduler.threads
2+
3+
package task
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 = Mutex

0 commit comments

Comments
 (0)