From 16409a2e512824ea8dc82ffaf18e2ade797c2a6a Mon Sep 17 00:00:00 2001 From: hkeni Date: Sun, 16 Nov 2025 17:50:35 -0500 Subject: [PATCH 1/7] Add goroutine core affinity support for RP2040/RP2350 systems - Introduced support for CPU core pinning and affinity for tasks and goroutines. - Updated the scheduler to respect affinity constraints with separate queues for pinned and shared tasks. - Added new runtime API functions `LockToCore`, `UnlockFromCore`, `GetAffinity`, and `CurrentCPU`. - Example program demonstrates core pinning and unpinned execution behavior. --- src/examples/core-pinning/main.go | 80 +++++++++++++++++++++ src/internal/task/task.go | 8 +++ src/runtime/scheduler_cores.go | 113 ++++++++++++++++++++++++++++-- 3 files changed, 194 insertions(+), 7 deletions(-) create mode 100644 src/examples/core-pinning/main.go diff --git a/src/examples/core-pinning/main.go b/src/examples/core-pinning/main.go new file mode 100644 index 0000000000..07c39efa20 --- /dev/null +++ b/src/examples/core-pinning/main.go @@ -0,0 +1,80 @@ +// This example demonstrates goroutine core pinning on multi-core systems (RP2040/RP2350). +// It shows how to pin goroutines to specific CPU cores and verify their execution. +// + +//go:build rp2040 || rp2350 + +package main + +import ( + "runtime" + "time" +) + +func main() { + println("=== Core Pinning Example ===") + println("Number of CPU cores:", runtime.NumCPU()) + println("Main starting on core:", runtime.CurrentCPU()) + println() + + // Pin main goroutine to core 0 + runtime.LockToCore(0) + println("Main pinned to core:", runtime.GetAffinity()) + println() + + // Start a goroutine pinned to core 1 + go core1Worker() + + // Start an unpinned goroutine (can run on either core) + go unpinnedWorker() + + // Main loop on core 0 + for i := 0; i < 10; i++ { + println("Core 0 (main):", i, "on CPU", runtime.CurrentCPU()) + time.Sleep(500 * time.Millisecond) + } + + // Unpin and let main run on any core + runtime.UnlockFromCore() + println() + println("Main unpinned, affinity:", runtime.GetAffinity()) + + // Continue running for a bit to show migration + for i := 0; i < 5; i++ { + println("Unpinned main on CPU", runtime.CurrentCPU()) + time.Sleep(500 * time.Millisecond) + } + + println() + println("Example complete!") +} + +// Worker function that runs on core 1 +func core1Worker() { + // Pin this goroutine to core 1 + runtime.LockToCore(1) + println("Worker pinned to core:", runtime.GetAffinity()) + + for i := 0; i < 10; i++ { + println(" Core 1 (worker):", i, "on CPU", runtime.CurrentCPU()) + time.Sleep(500 * time.Millisecond) + } + + println(" Core 1 worker finished") +} + +// Worker function that is not pinned (can run on any core) +func unpinnedWorker() { + println("Unpinned worker starting, affinity:", runtime.GetAffinity()) + + for i := 0; i < 10; i++ { + cpu := runtime.CurrentCPU() + println(" Unpinned worker:", i, "on CPU", cpu) + time.Sleep(700 * time.Millisecond) + + // Yield to potentially migrate to another core + runtime.Gosched() + } + + println(" Unpinned worker finished") +} diff --git a/src/internal/task/task.go b/src/internal/task/task.go index e257e1bc8e..40b0c3f3ae 100644 --- a/src/internal/task/task.go +++ b/src/internal/task/task.go @@ -29,6 +29,14 @@ type Task struct { // since it falls into the padding of the FipsIndicator bit above. RunState uint8 + // Affinity specifies which CPU core this task should run on. + // -1 means no affinity (can run on any core) + // 0, 1, etc. means pinned to that specific core + // To be used ONLY with the "cores" scheduler. + // By default, all goroutines are unpinned (Affinity = -1) + // Pinning takes effect at the next scheduling point (e.g., after time.Sleep(), channel operations, or runtime.Gosched()) + Affinity int8 + // DeferFrame stores a pointer to the (stack allocated) defer frame of the // goroutine that is used for the recover builtin. DeferFrame unsafe.Pointer diff --git a/src/runtime/scheduler_cores.go b/src/runtime/scheduler_cores.go index c2736cafd2..9f4871d08c 100644 --- a/src/runtime/scheduler_cores.go +++ b/src/runtime/scheduler_cores.go @@ -22,8 +22,9 @@ var secondaryCoresStarted bool var cpuTasks [numCPU]*task.Task var ( - sleepQueue *task.Task - runqueue task.Queue + sleepQueue *task.Task + runqueueShared task.Queue // For unpinned tasks (affinity = -1) + runqueueCore [numCPU]task.Queue // Per-core queues for pinned tasks ) func deadlock() { @@ -39,8 +40,14 @@ func scheduleTask(t *task.Task) { switch t.RunState { case task.RunStatePaused: // Paused, state is saved on the stack. - // Add it to the runqueue... - runqueue.Push(t) + // Route to appropriate queue based on affinity. + if t.Affinity >= 0 && t.Affinity < numCPU { + // Pinned to specific core + runqueueCore[t.Affinity].Push(t) + } else { + // Not pinned, use shared queue + runqueueShared.Push(t) + } // ...and wake up a sleeping core, if there is one. // (If all cores are already busy, this is a no-op). schedulerWake() @@ -86,7 +93,15 @@ func addSleepTask(t *task.Task, wakeup timeUnit) { func Gosched() { schedulerLock.Lock() - runqueue.Push(task.Current()) + t := task.Current() + + // Respect affinity when re-queueing. + if t.Affinity >= 0 && t.Affinity < numCPU { + runqueueCore[t.Affinity].Push(t) + } else { + runqueueShared.Push(t) + } + task.PauseLocked() } @@ -95,6 +110,53 @@ func NumCPU() int { return numCPU } +// CurrentCPU returns the current CPU core number. +// On RP2040/RP2350, this returns 0 or 1. +func CurrentCPU() int { + return int(currentCPU()) +} + +// LockToCore pins the current goroutine to the specified CPU core. +// Use core = -1 to unpin (allow running on any core). +// Use core = 0 or 1 to pin to a specific core. +// Panics if core is invalid (not -1, 0, or 1 on RP2040/RP2350). +func LockToCore(core int) { + if core < -1 || core >= numCPU { + panic("runtime: invalid core number") + } + + schedulerLock.Lock() + t := task.Current() + if t != nil { + t.Affinity = int8(core) + } + schedulerLock.Unlock() +} + +// UnlockFromCore unpins the current goroutine, allowing it to run on any core. +// This is equivalent to LockToCore(-1). +func UnlockFromCore() { + schedulerLock.Lock() + t := task.Current() + if t != nil { + t.Affinity = -1 + } + schedulerLock.Unlock() +} + +// GetAffinity returns the CPU core affinity of the current goroutine. +// Returns -1 if not pinned, or 0/1 if pinned to a specific core. +func GetAffinity() int { + schedulerLock.Lock() + t := task.Current() + affinity := -1 + if t != nil { + affinity = int(t.Affinity) + } + schedulerLock.Unlock() + return affinity +} + func addTimer(tn *timerNode) { schedulerLock.Lock() timerQueueAdd(tn) @@ -110,7 +172,7 @@ func removeTimer(t *timer) *timerNode { } func schedulerRunQueue() *task.Queue { - return &runqueue + return &runqueueShared } // Pause the current task for a given time. @@ -160,9 +222,33 @@ func run() { } func scheduler(_ bool) { + currentCore := int(currentCPU()) + for mainExited.Load() == 0 { // Check for ready-to-run tasks. - if runnable := runqueue.Pop(); runnable != nil { + // First, try to get a task pinned to this core. + var runnable *task.Task + if currentCore < numCPU { + runnable = runqueueCore[currentCore].Pop() + } + + // If no pinned tasks, try the shared queue. + if runnable == nil { + runnable = runqueueShared.Pop() + } + + if runnable != nil { + // Verify affinity constraint (sanity check). + if runnable.Affinity >= 0 && runnable.Affinity != int8(currentCore) { + // Shouldn't happen, but put it back on correct queue. + if runnable.Affinity < numCPU { + runqueueCore[runnable.Affinity].Push(runnable) + } else { + runqueueShared.Push(runnable) + } + continue + } + // Resume it now. setCurrentTask(runnable) runnable.RunState = task.RunStateRunning @@ -183,6 +269,19 @@ func scheduler(_ bool) { sleepQueue = sleepQueue.Next sleepingTask.Next = nil + // Check affinity before running. + if sleepingTask.Affinity >= 0 && sleepingTask.Affinity != int8(currentCore) { + // Task is pinned to a different core, re-queue it. + sleepingTask.RunState = task.RunStatePaused + if sleepingTask.Affinity < numCPU { + runqueueCore[sleepingTask.Affinity].Push(sleepingTask) + } else { + runqueueShared.Push(sleepingTask) + } + schedulerWake() + continue + } + // Run it now. setCurrentTask(sleepingTask) sleepingTask.RunState = task.RunStateRunning From 870af4f279246ba6542e128ad4baede277f5619e Mon Sep 17 00:00:00 2001 From: hkeni Date: Tue, 18 Nov 2025 09:23:09 -0500 Subject: [PATCH 2/7] Refactored based on Elias' comments - Dropped CurrentCPU - Dropped GetAffinity - Renamed LockToCore to LockCore to mimic LockOSThread naming. - Updated examples program --- src/examples/core-pinning/main.go | 71 ++++++++++++++++++++-------- src/machine/machine_rp2_cores.go | 26 ++++++++++ src/machine/machine_rp2_nocores.go | 15 ++++++ src/runtime/runtime.go | 8 +++- src/runtime/scheduler_cooperative.go | 10 ++++ src/runtime/scheduler_cores.go | 52 ++++++++++---------- src/runtime/scheduler_none.go | 10 ++++ src/runtime/scheduler_threads.go | 10 ++++ 8 files changed, 152 insertions(+), 50 deletions(-) create mode 100644 src/machine/machine_rp2_cores.go create mode 100644 src/machine/machine_rp2_nocores.go diff --git a/src/examples/core-pinning/main.go b/src/examples/core-pinning/main.go index 07c39efa20..94e7ae1b06 100644 --- a/src/examples/core-pinning/main.go +++ b/src/examples/core-pinning/main.go @@ -1,47 +1,60 @@ // This example demonstrates goroutine core pinning on multi-core systems (RP2040/RP2350). // It shows how to pin goroutines to specific CPU cores and verify their execution. -// //go:build rp2040 || rp2350 package main import ( + "machine" "runtime" "time" ) func main() { + time.Sleep(5 * time.Second) println("=== Core Pinning Example ===") println("Number of CPU cores:", runtime.NumCPU()) - println("Main starting on core:", runtime.CurrentCPU()) + println("[main] Main starting on core:", machine.CurrentCore()) println() - // Pin main goroutine to core 0 - runtime.LockToCore(0) - println("Main pinned to core:", runtime.GetAffinity()) + // Example 1: Pin using standard Go API (LockOSThread) + // This pins to whichever core this goroutine is currently running on + runtime.LockOSThread() + println("[main] Pinned using runtime.LockOSThread()") + println("[main] Running on core:", machine.CurrentCore()) + runtime.UnlockOSThread() + println("[main] Unpinned using runtime.UnlockOSThread()") + println() + + // Example 2: Pin to a specific core using machine package + machine.LockCore(0) + println("[main] Explicitly pinned to core 0 using machine.LockCore()") println() // Start a goroutine pinned to core 1 go core1Worker() + // Start a goroutine using standard LockOSThread + go standardLockWorker() + // Start an unpinned goroutine (can run on either core) go unpinnedWorker() // Main loop on core 0 for i := 0; i < 10; i++ { - println("Core 0 (main):", i, "on CPU", runtime.CurrentCPU()) + println("[main] loop", i, "on CPU", machine.CurrentCore()) time.Sleep(500 * time.Millisecond) } // Unpin and let main run on any core - runtime.UnlockFromCore() + machine.UnlockCore() println() - println("Main unpinned, affinity:", runtime.GetAffinity()) + println("[main] Unpinned using machine.UnlockCore()") - // Continue running for a bit to show migration + // Continue running for a bit to show potential migration for i := 0; i < 5; i++ { - println("Unpinned main on CPU", runtime.CurrentCPU()) + println("[main] unpinned loop on CPU", machine.CurrentCore()) time.Sleep(500 * time.Millisecond) } @@ -49,32 +62,50 @@ func main() { println("Example complete!") } -// Worker function that runs on core 1 +// Worker function that pins to core 1 using explicit core selection func core1Worker() { - // Pin this goroutine to core 1 - runtime.LockToCore(1) - println("Worker pinned to core:", runtime.GetAffinity()) + // Pin this goroutine to core 1 explicitly + machine.LockCore(1) + println("[core1-worker] Worker pinned to core 1 using machine.LockCore()") for i := 0; i < 10; i++ { - println(" Core 1 (worker):", i, "on CPU", runtime.CurrentCPU()) + println("[core1-worker] loop", i, "on CPU", machine.CurrentCore()) time.Sleep(500 * time.Millisecond) } - println(" Core 1 worker finished") + println("[core1-worker] Finished") +} + +// Worker function that uses standard Go LockOSThread() +func standardLockWorker() { + // Pin this goroutine to whichever core it starts on + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + core := machine.CurrentCore() + println("[std-lock-worker] Worker locked using runtime.LockOSThread()") + println("[std-lock-worker] Running on core:", core) + + for i := 0; i < 10; i++ { + println("[std-lock-worker] loop", i, "on CPU", machine.CurrentCore()) + time.Sleep(600 * time.Millisecond) + } + + println("[std-lock-worker] Finished") } // Worker function that is not pinned (can run on any core) func unpinnedWorker() { - println("Unpinned worker starting, affinity:", runtime.GetAffinity()) + println("[unpinned-worker] Starting") for i := 0; i < 10; i++ { - cpu := runtime.CurrentCPU() - println(" Unpinned worker:", i, "on CPU", cpu) + cpu := machine.CurrentCore() + println("[unpinned-worker] loop", i, "on CPU", cpu) time.Sleep(700 * time.Millisecond) // Yield to potentially migrate to another core runtime.Gosched() } - println(" Unpinned worker finished") + println("[unpinned-worker] Finished") } diff --git a/src/machine/machine_rp2_cores.go b/src/machine/machine_rp2_cores.go new file mode 100644 index 0000000000..fda934dade --- /dev/null +++ b/src/machine/machine_rp2_cores.go @@ -0,0 +1,26 @@ +//go:build (rp2040 || rp2350) && scheduler.cores + +package machine + +const numCPU = 2 // RP2040 and RP2350 both have 2 cores + +// LockCore implementation for the cores scheduler. +func LockCore(core int) { + if core < 0 || core >= numCPU { + panic("machine: core out of range") + } + machineLockCore(core) +} + +// UnlockCore implementation for the cores scheduler. +func UnlockCore() { + machineUnlockCore() +} + +// Internal functions implemented in runtime/scheduler_cores.go +// +//go:linkname machineLockCore runtime.machineLockCore +func machineLockCore(core int) + +//go:linkname machineUnlockCore runtime.machineUnlockCore +func machineUnlockCore() diff --git a/src/machine/machine_rp2_nocores.go b/src/machine/machine_rp2_nocores.go new file mode 100644 index 0000000000..ac22544938 --- /dev/null +++ b/src/machine/machine_rp2_nocores.go @@ -0,0 +1,15 @@ +//go:build (rp2040 || rp2350) && !scheduler.cores + +package machine + +// LockCore is not available without the cores scheduler. +// This is a stub that panics. +func LockCore(core int) { + panic("machine.LockCore: not available without scheduler.cores") +} + +// UnlockCore is not available without the cores scheduler. +// This is a stub that panics. +func UnlockCore() { + panic("machine.UnlockCore: not available without scheduler.cores") +} diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go index c9b0959384..b10a4bdf4b 100644 --- a/src/runtime/runtime.go +++ b/src/runtime/runtime.go @@ -98,14 +98,18 @@ func os_sigpipe() { } // LockOSThread wires the calling goroutine to its current operating system thread. -// Stub for now +// On microcontrollers with multiple cores (e.g., RP2040/RP2350), this pins the +// goroutine to the core it's currently running on. // Called by go1.18 standard library on windows, see https://github.com/golang/go/issues/49320 func LockOSThread() { + lockOSThreadImpl() } // UnlockOSThread undoes an earlier call to LockOSThread. -// Stub for now +// On microcontrollers with multiple cores, this unpins the goroutine, allowing +// it to run on any available core. func UnlockOSThread() { + unlockOSThreadImpl() } // KeepAlive makes sure the value in the interface is alive until at least the diff --git a/src/runtime/scheduler_cooperative.go b/src/runtime/scheduler_cooperative.go index 274daa84d5..56f7c3b8c7 100644 --- a/src/runtime/scheduler_cooperative.go +++ b/src/runtime/scheduler_cooperative.go @@ -261,6 +261,16 @@ func unlockAtomics(mask interrupt.State) { interrupt.Restore(mask) } +// lockOSThreadImpl is a no-op for the cooperative scheduler (single-threaded). +func lockOSThreadImpl() { + // Single-threaded, nothing to do. +} + +// unlockOSThreadImpl is a no-op for the cooperative scheduler (single-threaded). +func unlockOSThreadImpl() { + // Single-threaded, nothing to do. +} + func printlock() { // nothing to do } diff --git a/src/runtime/scheduler_cores.go b/src/runtime/scheduler_cores.go index 9f4871d08c..8d6a82b346 100644 --- a/src/runtime/scheduler_cores.go +++ b/src/runtime/scheduler_cores.go @@ -110,21 +110,18 @@ func NumCPU() int { return numCPU } -// CurrentCPU returns the current CPU core number. -// On RP2040/RP2350, this returns 0 or 1. -func CurrentCPU() int { - return int(currentCPU()) -} - -// LockToCore pins the current goroutine to the specified CPU core. -// Use core = -1 to unpin (allow running on any core). -// Use core = 0 or 1 to pin to a specific core. -// Panics if core is invalid (not -1, 0, or 1 on RP2040/RP2350). -func LockToCore(core int) { - if core < -1 || core >= numCPU { - panic("runtime: invalid core number") - } +// +// Warning: Pinning goroutines can lead to load imbalance. The goroutine will +// wait in the specified core's queue even if other cores are idle. Use this +// feature carefully and only when you need explicit core affinity. +// +// Valid core values are 0 and 1. Panics if core is out of range. +// +// machineLockCore pins the current goroutine to the specified CPU core. +// This is called by machine.LockCore() on RP2040/RP2350. +// It does not validate the core number - validation is done in machine package. +func machineLockCore(core int) { schedulerLock.Lock() t := task.Current() if t != nil { @@ -133,9 +130,9 @@ func LockToCore(core int) { schedulerLock.Unlock() } -// UnlockFromCore unpins the current goroutine, allowing it to run on any core. -// This is equivalent to LockToCore(-1). -func UnlockFromCore() { +// machineUnlockCore unpins the current goroutine. +// This is called by machine.UnlockCore() on RP2040/RP2350. +func machineUnlockCore() { schedulerLock.Lock() t := task.Current() if t != nil { @@ -144,17 +141,16 @@ func UnlockFromCore() { schedulerLock.Unlock() } -// GetAffinity returns the CPU core affinity of the current goroutine. -// Returns -1 if not pinned, or 0/1 if pinned to a specific core. -func GetAffinity() int { - schedulerLock.Lock() - t := task.Current() - affinity := -1 - if t != nil { - affinity = int(t.Affinity) - } - schedulerLock.Unlock() - return affinity +// lockOSThreadImpl implements LockOSThread for the cores scheduler. +// It pins the current goroutine to whichever core it's currently running on. +func lockOSThreadImpl() { + core := int(currentCPU()) + machineLockCore(core) +} + +// unlockOSThreadImpl implements UnlockOSThread for the cores scheduler. +func unlockOSThreadImpl() { + machineUnlockCore() } func addTimer(tn *timerNode) { diff --git a/src/runtime/scheduler_none.go b/src/runtime/scheduler_none.go index 06722afcf8..c938b5c781 100644 --- a/src/runtime/scheduler_none.go +++ b/src/runtime/scheduler_none.go @@ -85,6 +85,16 @@ func unlockAtomics(mask interrupt.State) { interrupt.Restore(mask) } +// lockOSThreadImpl is a no-op for the cooperative scheduler (single-threaded). +func lockOSThreadImpl() { + // Single-threaded, nothing to do. +} + +// unlockOSThreadImpl is a no-op for the cooperative scheduler (single-threaded). +func unlockOSThreadImpl() { + // Single-threaded, nothing to do. +} + func printlock() { // nothing to do } diff --git a/src/runtime/scheduler_threads.go b/src/runtime/scheduler_threads.go index 6d41d0c99e..52a3ab3782 100644 --- a/src/runtime/scheduler_threads.go +++ b/src/runtime/scheduler_threads.go @@ -157,3 +157,13 @@ func lockAtomics() interrupt.State { func unlockAtomics(mask interrupt.State) { atomicsLock.Unlock() } + +// lockOSThreadImpl is a no-op for the cooperative scheduler (single-threaded). +func lockOSThreadImpl() { + // Single-threaded, nothing to do. +} + +// unlockOSThreadImpl is a no-op for the cooperative scheduler (single-threaded). +func unlockOSThreadImpl() { + // Single-threaded, nothing to do. +} From c00fc8487c5d5098a03b20aa0455ab78ffe386ed Mon Sep 17 00:00:00 2001 From: hkeni Date: Wed, 19 Nov 2025 08:13:52 -0500 Subject: [PATCH 3/7] Improved LockCore and UnlockCore documentation for clarity on usage, behavior, and limitations with the "cores" scheduler. Updated LockOSThread and UnlockOSThread comments to reflect core pinning behavior on RP2040/RP2350. --- src/machine/machine_rp2_cores.go | 35 ++++++++++++++++++++++++++++++-- src/runtime/runtime.go | 5 +++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/machine/machine_rp2_cores.go b/src/machine/machine_rp2_cores.go index fda934dade..2e4f91cd17 100644 --- a/src/machine/machine_rp2_cores.go +++ b/src/machine/machine_rp2_cores.go @@ -4,7 +4,32 @@ package machine const numCPU = 2 // RP2040 and RP2350 both have 2 cores -// LockCore implementation for the cores scheduler. +// LockCore sets the affinity for the current goroutine to the specified core. +// This does not immediately migrate the goroutine; migration occurs at the next +// scheduling point. See machine_rp2.go for full documentation. +// Important: LockCore sets the affinity but does not immediately migrate the +// goroutine to the target core. The actual migration happens at the next +// scheduling point (e.g., channel operation, time.Sleep, or Gosched). After +// that point, the goroutine will wait in the target core's queue if that core +// is busy running another goroutine. +// +// To avoid potential blocking on a busy core, consider calling LockCore in an +// init function before any other goroutines have started. This guarantees the +// target core is available. +// +// This is useful for: +// - Isolating time-critical operations to a dedicated core +// - Improving cache locality for performance-sensitive code +// - Exclusive access to core-local resources +// +// Warning: Pinning goroutines can lead to load imbalance. The goroutine will +// wait in the specified core's queue even if other cores are idle. If a +// long-running goroutine occupies the target core, LockCore may appear to +// block indefinitely (until the next scheduling point on the target core). +// +// Valid core values are 0 and 1. Panics if core is out of range. +// +// Only available on RP2040 and RP2350 with the "cores" scheduler. func LockCore(core int) { if core < 0 || core >= numCPU { panic("machine: core out of range") @@ -12,7 +37,13 @@ func LockCore(core int) { machineLockCore(core) } -// UnlockCore implementation for the cores scheduler. +// UnlockCore unpins the calling goroutine, allowing it to run on any available core. +// This undoes a previous call to LockCore. +// +// After calling UnlockCore, the scheduler is free to schedule the goroutine on +// any core for automatic load balancing. +// +// Only available on RP2040 and RP2350 with the "cores" scheduler. func UnlockCore() { machineUnlockCore() } diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go index b10a4bdf4b..eb97b1c1f5 100644 --- a/src/runtime/runtime.go +++ b/src/runtime/runtime.go @@ -100,6 +100,9 @@ func os_sigpipe() { // LockOSThread wires the calling goroutine to its current operating system thread. // On microcontrollers with multiple cores (e.g., RP2040/RP2350), this pins the // goroutine to the core it's currently running on. +// With the "cores" scheduler on RP2040/RP2350, this pins the goroutine to the +// core it's currently running on. The pinning takes effect at the next +// scheduling point (e.g., channel operation, time.Sleep, or Gosched). // Called by go1.18 standard library on windows, see https://github.com/golang/go/issues/49320 func LockOSThread() { lockOSThreadImpl() @@ -108,6 +111,8 @@ func LockOSThread() { // UnlockOSThread undoes an earlier call to LockOSThread. // On microcontrollers with multiple cores, this unpins the goroutine, allowing // it to run on any available core. +// With the "cores" scheduler, this unpins the goroutine, allowing it to run on +// any available core. func UnlockOSThread() { unlockOSThreadImpl() } From 90250537dc25502eecbd2b0c19a2b4bfed98c085 Mon Sep 17 00:00:00 2001 From: hkeni Date: Sat, 22 Nov 2025 15:56:43 -0500 Subject: [PATCH 4/7] Add runtime.Gosched call in LockCore for improved scheduling --- src/machine/machine_rp2_cores.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/machine/machine_rp2_cores.go b/src/machine/machine_rp2_cores.go index 2e4f91cd17..021b1e2866 100644 --- a/src/machine/machine_rp2_cores.go +++ b/src/machine/machine_rp2_cores.go @@ -2,6 +2,8 @@ package machine +import "runtime" + const numCPU = 2 // RP2040 and RP2350 both have 2 cores // LockCore sets the affinity for the current goroutine to the specified core. @@ -35,6 +37,7 @@ func LockCore(core int) { panic("machine: core out of range") } machineLockCore(core) + runtime.Gosched() } // UnlockCore unpins the calling goroutine, allowing it to run on any available core. From 8820b4e9159315fa9ff015efb7760c23101ec752 Mon Sep 17 00:00:00 2001 From: hkeni Date: Sat, 22 Nov 2025 15:59:21 -0500 Subject: [PATCH 5/7] Removed superfluous comments --- src/machine/machine_rp2_cores.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/machine/machine_rp2_cores.go b/src/machine/machine_rp2_cores.go index 021b1e2866..514ded9a86 100644 --- a/src/machine/machine_rp2_cores.go +++ b/src/machine/machine_rp2_cores.go @@ -9,11 +9,6 @@ const numCPU = 2 // RP2040 and RP2350 both have 2 cores // LockCore sets the affinity for the current goroutine to the specified core. // This does not immediately migrate the goroutine; migration occurs at the next // scheduling point. See machine_rp2.go for full documentation. -// Important: LockCore sets the affinity but does not immediately migrate the -// goroutine to the target core. The actual migration happens at the next -// scheduling point (e.g., channel operation, time.Sleep, or Gosched). After -// that point, the goroutine will wait in the target core's queue if that core -// is busy running another goroutine. // // To avoid potential blocking on a busy core, consider calling LockCore in an // init function before any other goroutines have started. This guarantees the @@ -28,10 +23,6 @@ const numCPU = 2 // RP2040 and RP2350 both have 2 cores // wait in the specified core's queue even if other cores are idle. If a // long-running goroutine occupies the target core, LockCore may appear to // block indefinitely (until the next scheduling point on the target core). -// -// Valid core values are 0 and 1. Panics if core is out of range. -// -// Only available on RP2040 and RP2350 with the "cores" scheduler. func LockCore(core int) { if core < 0 || core >= numCPU { panic("machine: core out of range") From 3ca2b71b3feafb67aa580d50606f63cc7ef75236 Mon Sep 17 00:00:00 2001 From: hkeni Date: Sat, 22 Nov 2025 16:21:04 -0500 Subject: [PATCH 6/7] Added GoSched() to scheduler_cores.go instead of incorrectly calling it in machine_rp2_cores.go --- src/machine/machine_rp2_cores.go | 1 - src/runtime/scheduler_cores.go | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/machine/machine_rp2_cores.go b/src/machine/machine_rp2_cores.go index 514ded9a86..97fce94a9a 100644 --- a/src/machine/machine_rp2_cores.go +++ b/src/machine/machine_rp2_cores.go @@ -28,7 +28,6 @@ func LockCore(core int) { panic("machine: core out of range") } machineLockCore(core) - runtime.Gosched() } // UnlockCore unpins the calling goroutine, allowing it to run on any available core. diff --git a/src/runtime/scheduler_cores.go b/src/runtime/scheduler_cores.go index 8d6a82b346..ac811a8529 100644 --- a/src/runtime/scheduler_cores.go +++ b/src/runtime/scheduler_cores.go @@ -128,6 +128,7 @@ func machineLockCore(core int) { t.Affinity = int8(core) } schedulerLock.Unlock() + Gosched() } // machineUnlockCore unpins the current goroutine. From 217adfb7e34b97a192459bb6e1a20b7510c78ddb Mon Sep 17 00:00:00 2001 From: hkeni Date: Sat, 22 Nov 2025 23:58:19 -0500 Subject: [PATCH 7/7] Remove unused import of "runtime" in machine_rp2_cores.go --- src/machine/machine_rp2_cores.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/machine/machine_rp2_cores.go b/src/machine/machine_rp2_cores.go index 97fce94a9a..87e0d1277e 100644 --- a/src/machine/machine_rp2_cores.go +++ b/src/machine/machine_rp2_cores.go @@ -2,8 +2,6 @@ package machine -import "runtime" - const numCPU = 2 // RP2040 and RP2350 both have 2 cores // LockCore sets the affinity for the current goroutine to the specified core.