Skip to content

Commit 870af4f

Browse files
committed
Refactored based on Elias' comments
- Dropped CurrentCPU - Dropped GetAffinity - Renamed LockToCore to LockCore to mimic LockOSThread naming. - Updated examples program
1 parent 16409a2 commit 870af4f

File tree

8 files changed

+152
-50
lines changed

8 files changed

+152
-50
lines changed

src/examples/core-pinning/main.go

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,111 @@
11
// This example demonstrates goroutine core pinning on multi-core systems (RP2040/RP2350).
22
// It shows how to pin goroutines to specific CPU cores and verify their execution.
3-
//
43

54
//go:build rp2040 || rp2350
65

76
package main
87

98
import (
9+
"machine"
1010
"runtime"
1111
"time"
1212
)
1313

1414
func main() {
15+
time.Sleep(5 * time.Second)
1516
println("=== Core Pinning Example ===")
1617
println("Number of CPU cores:", runtime.NumCPU())
17-
println("Main starting on core:", runtime.CurrentCPU())
18+
println("[main] Main starting on core:", machine.CurrentCore())
1819
println()
1920

20-
// Pin main goroutine to core 0
21-
runtime.LockToCore(0)
22-
println("Main pinned to core:", runtime.GetAffinity())
21+
// Example 1: Pin using standard Go API (LockOSThread)
22+
// This pins to whichever core this goroutine is currently running on
23+
runtime.LockOSThread()
24+
println("[main] Pinned using runtime.LockOSThread()")
25+
println("[main] Running on core:", machine.CurrentCore())
26+
runtime.UnlockOSThread()
27+
println("[main] Unpinned using runtime.UnlockOSThread()")
28+
println()
29+
30+
// Example 2: Pin to a specific core using machine package
31+
machine.LockCore(0)
32+
println("[main] Explicitly pinned to core 0 using machine.LockCore()")
2333
println()
2434

2535
// Start a goroutine pinned to core 1
2636
go core1Worker()
2737

38+
// Start a goroutine using standard LockOSThread
39+
go standardLockWorker()
40+
2841
// Start an unpinned goroutine (can run on either core)
2942
go unpinnedWorker()
3043

3144
// Main loop on core 0
3245
for i := 0; i < 10; i++ {
33-
println("Core 0 (main):", i, "on CPU", runtime.CurrentCPU())
46+
println("[main] loop", i, "on CPU", machine.CurrentCore())
3447
time.Sleep(500 * time.Millisecond)
3548
}
3649

3750
// Unpin and let main run on any core
38-
runtime.UnlockFromCore()
51+
machine.UnlockCore()
3952
println()
40-
println("Main unpinned, affinity:", runtime.GetAffinity())
53+
println("[main] Unpinned using machine.UnlockCore()")
4154

42-
// Continue running for a bit to show migration
55+
// Continue running for a bit to show potential migration
4356
for i := 0; i < 5; i++ {
44-
println("Unpinned main on CPU", runtime.CurrentCPU())
57+
println("[main] unpinned loop on CPU", machine.CurrentCore())
4558
time.Sleep(500 * time.Millisecond)
4659
}
4760

4861
println()
4962
println("Example complete!")
5063
}
5164

52-
// Worker function that runs on core 1
65+
// Worker function that pins to core 1 using explicit core selection
5366
func core1Worker() {
54-
// Pin this goroutine to core 1
55-
runtime.LockToCore(1)
56-
println("Worker pinned to core:", runtime.GetAffinity())
67+
// Pin this goroutine to core 1 explicitly
68+
machine.LockCore(1)
69+
println("[core1-worker] Worker pinned to core 1 using machine.LockCore()")
5770

5871
for i := 0; i < 10; i++ {
59-
println(" Core 1 (worker):", i, "on CPU", runtime.CurrentCPU())
72+
println("[core1-worker] loop", i, "on CPU", machine.CurrentCore())
6073
time.Sleep(500 * time.Millisecond)
6174
}
6275

63-
println(" Core 1 worker finished")
76+
println("[core1-worker] Finished")
77+
}
78+
79+
// Worker function that uses standard Go LockOSThread()
80+
func standardLockWorker() {
81+
// Pin this goroutine to whichever core it starts on
82+
runtime.LockOSThread()
83+
defer runtime.UnlockOSThread()
84+
85+
core := machine.CurrentCore()
86+
println("[std-lock-worker] Worker locked using runtime.LockOSThread()")
87+
println("[std-lock-worker] Running on core:", core)
88+
89+
for i := 0; i < 10; i++ {
90+
println("[std-lock-worker] loop", i, "on CPU", machine.CurrentCore())
91+
time.Sleep(600 * time.Millisecond)
92+
}
93+
94+
println("[std-lock-worker] Finished")
6495
}
6596

6697
// Worker function that is not pinned (can run on any core)
6798
func unpinnedWorker() {
68-
println("Unpinned worker starting, affinity:", runtime.GetAffinity())
99+
println("[unpinned-worker] Starting")
69100

70101
for i := 0; i < 10; i++ {
71-
cpu := runtime.CurrentCPU()
72-
println(" Unpinned worker:", i, "on CPU", cpu)
102+
cpu := machine.CurrentCore()
103+
println("[unpinned-worker] loop", i, "on CPU", cpu)
73104
time.Sleep(700 * time.Millisecond)
74105

75106
// Yield to potentially migrate to another core
76107
runtime.Gosched()
77108
}
78109

79-
println(" Unpinned worker finished")
110+
println("[unpinned-worker] Finished")
80111
}

src/machine/machine_rp2_cores.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//go:build (rp2040 || rp2350) && scheduler.cores
2+
3+
package machine
4+
5+
const numCPU = 2 // RP2040 and RP2350 both have 2 cores
6+
7+
// LockCore implementation for the cores scheduler.
8+
func LockCore(core int) {
9+
if core < 0 || core >= numCPU {
10+
panic("machine: core out of range")
11+
}
12+
machineLockCore(core)
13+
}
14+
15+
// UnlockCore implementation for the cores scheduler.
16+
func UnlockCore() {
17+
machineUnlockCore()
18+
}
19+
20+
// Internal functions implemented in runtime/scheduler_cores.go
21+
//
22+
//go:linkname machineLockCore runtime.machineLockCore
23+
func machineLockCore(core int)
24+
25+
//go:linkname machineUnlockCore runtime.machineUnlockCore
26+
func machineUnlockCore()

src/machine/machine_rp2_nocores.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//go:build (rp2040 || rp2350) && !scheduler.cores
2+
3+
package machine
4+
5+
// LockCore is not available without the cores scheduler.
6+
// This is a stub that panics.
7+
func LockCore(core int) {
8+
panic("machine.LockCore: not available without scheduler.cores")
9+
}
10+
11+
// UnlockCore is not available without the cores scheduler.
12+
// This is a stub that panics.
13+
func UnlockCore() {
14+
panic("machine.UnlockCore: not available without scheduler.cores")
15+
}

src/runtime/runtime.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,18 @@ func os_sigpipe() {
9898
}
9999

100100
// LockOSThread wires the calling goroutine to its current operating system thread.
101-
// Stub for now
101+
// On microcontrollers with multiple cores (e.g., RP2040/RP2350), this pins the
102+
// goroutine to the core it's currently running on.
102103
// Called by go1.18 standard library on windows, see https://github.com/golang/go/issues/49320
103104
func LockOSThread() {
105+
lockOSThreadImpl()
104106
}
105107

106108
// UnlockOSThread undoes an earlier call to LockOSThread.
107-
// Stub for now
109+
// On microcontrollers with multiple cores, this unpins the goroutine, allowing
110+
// it to run on any available core.
108111
func UnlockOSThread() {
112+
unlockOSThreadImpl()
109113
}
110114

111115
// KeepAlive makes sure the value in the interface is alive until at least the

src/runtime/scheduler_cooperative.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,16 @@ func unlockAtomics(mask interrupt.State) {
261261
interrupt.Restore(mask)
262262
}
263263

264+
// lockOSThreadImpl is a no-op for the cooperative scheduler (single-threaded).
265+
func lockOSThreadImpl() {
266+
// Single-threaded, nothing to do.
267+
}
268+
269+
// unlockOSThreadImpl is a no-op for the cooperative scheduler (single-threaded).
270+
func unlockOSThreadImpl() {
271+
// Single-threaded, nothing to do.
272+
}
273+
264274
func printlock() {
265275
// nothing to do
266276
}

src/runtime/scheduler_cores.go

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -110,21 +110,18 @@ func NumCPU() int {
110110
return numCPU
111111
}
112112

113-
// CurrentCPU returns the current CPU core number.
114-
// On RP2040/RP2350, this returns 0 or 1.
115-
func CurrentCPU() int {
116-
return int(currentCPU())
117-
}
118-
119-
// LockToCore pins the current goroutine to the specified CPU core.
120-
// Use core = -1 to unpin (allow running on any core).
121-
// Use core = 0 or 1 to pin to a specific core.
122-
// Panics if core is invalid (not -1, 0, or 1 on RP2040/RP2350).
123-
func LockToCore(core int) {
124-
if core < -1 || core >= numCPU {
125-
panic("runtime: invalid core number")
126-
}
113+
//
114+
// Warning: Pinning goroutines can lead to load imbalance. The goroutine will
115+
// wait in the specified core's queue even if other cores are idle. Use this
116+
// feature carefully and only when you need explicit core affinity.
117+
//
118+
// Valid core values are 0 and 1. Panics if core is out of range.
119+
//
127120

121+
// machineLockCore pins the current goroutine to the specified CPU core.
122+
// This is called by machine.LockCore() on RP2040/RP2350.
123+
// It does not validate the core number - validation is done in machine package.
124+
func machineLockCore(core int) {
128125
schedulerLock.Lock()
129126
t := task.Current()
130127
if t != nil {
@@ -133,9 +130,9 @@ func LockToCore(core int) {
133130
schedulerLock.Unlock()
134131
}
135132

136-
// UnlockFromCore unpins the current goroutine, allowing it to run on any core.
137-
// This is equivalent to LockToCore(-1).
138-
func UnlockFromCore() {
133+
// machineUnlockCore unpins the current goroutine.
134+
// This is called by machine.UnlockCore() on RP2040/RP2350.
135+
func machineUnlockCore() {
139136
schedulerLock.Lock()
140137
t := task.Current()
141138
if t != nil {
@@ -144,17 +141,16 @@ func UnlockFromCore() {
144141
schedulerLock.Unlock()
145142
}
146143

147-
// GetAffinity returns the CPU core affinity of the current goroutine.
148-
// Returns -1 if not pinned, or 0/1 if pinned to a specific core.
149-
func GetAffinity() int {
150-
schedulerLock.Lock()
151-
t := task.Current()
152-
affinity := -1
153-
if t != nil {
154-
affinity = int(t.Affinity)
155-
}
156-
schedulerLock.Unlock()
157-
return affinity
144+
// lockOSThreadImpl implements LockOSThread for the cores scheduler.
145+
// It pins the current goroutine to whichever core it's currently running on.
146+
func lockOSThreadImpl() {
147+
core := int(currentCPU())
148+
machineLockCore(core)
149+
}
150+
151+
// unlockOSThreadImpl implements UnlockOSThread for the cores scheduler.
152+
func unlockOSThreadImpl() {
153+
machineUnlockCore()
158154
}
159155

160156
func addTimer(tn *timerNode) {

src/runtime/scheduler_none.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ func unlockAtomics(mask interrupt.State) {
8585
interrupt.Restore(mask)
8686
}
8787

88+
// lockOSThreadImpl is a no-op for the cooperative scheduler (single-threaded).
89+
func lockOSThreadImpl() {
90+
// Single-threaded, nothing to do.
91+
}
92+
93+
// unlockOSThreadImpl is a no-op for the cooperative scheduler (single-threaded).
94+
func unlockOSThreadImpl() {
95+
// Single-threaded, nothing to do.
96+
}
97+
8898
func printlock() {
8999
// nothing to do
90100
}

src/runtime/scheduler_threads.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,13 @@ func lockAtomics() interrupt.State {
157157
func unlockAtomics(mask interrupt.State) {
158158
atomicsLock.Unlock()
159159
}
160+
161+
// lockOSThreadImpl is a no-op for the cooperative scheduler (single-threaded).
162+
func lockOSThreadImpl() {
163+
// Single-threaded, nothing to do.
164+
}
165+
166+
// unlockOSThreadImpl is a no-op for the cooperative scheduler (single-threaded).
167+
func unlockOSThreadImpl() {
168+
// Single-threaded, nothing to do.
169+
}

0 commit comments

Comments
 (0)