Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Add goroutine core affinity support for RP2040/RP2350 systems#5092

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
amken3d wants to merge7 commits intotinygo-org:dev
base:dev
Choose a base branch
Loading
fromamken3d:dev-rp2-core-pinning

Conversation

@amken3d
Copy link

This PR proposes

  • 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 functionsLockToCore,UnlockFromCore,GetAffinity, andCurrentCPU.
  • Example program demonstrates core pinning and unpinned execution behavior.

API Functions

runtime.NumCPU() int

Returns the number of CPU cores available (returns 2 on RP2040/RP2350).

runtime.CurrentCPU() int

Returns the current CPU core number (0 or 1).

runtime.LockToCore(core int)

Pins the current goroutine to the specified core:

  • core = 0 - Pin to core 0
  • core = 1 - Pin to core 1
  • core = -1 - Unpin (allow running on any core)

Panics if core is invalid (not -1, 0, or 1).

runtime.UnlockFromCore()

Unpins the current goroutine, allowing it to run on any core.
Equivalent toruntime.LockToCore(-1).

runtime.GetAffinity() int

Returns the current goroutine's CPU affinity:

  • Returns-1 if not pinned (can run on any core)
  • Returns0 or1 if pinned to that specific core

Example program included in the examples directory

  • Tested on both pico and pico2

  • Output of example program

=== Core Pinning Example ===                                             Number of CPU cores: 2                                                   Main starting on core: 0                                                                                                                          Main pinned to core: 0Core 0 (main): 0 on CPU 0Worker pinned to core: 1  Core 1 (worker): 0 on CPU 0Unpinned worker starting, affinity: 0    Unpinned worker: 0 on CPU 0Core 0 (main): 1 on CPU 0    Unpinned worker: 1 on CPU 0Core 0 (main): 2 on CPU 0  Core 1 (worker): 2 on CPU 1    Unpinned worker: 2 on CPU 0                                                 Core 0 (main): 3 on CPU 0                                                         Core 1 (worker): 3 onCPU 1                                                    Core 0 (main): 4 on CPU 0                                                         Core 1 (worker): 4 on CPU 1                                                       Unpinned worker: 3 on CPU 0                                                 Core 0 (main): 5 on CPU 0                                                         Core 1 (worker): 5 on CPU 1                                                       Unpinned worker: 4 on CPU 0                                                 Core 0 (main): 6 on CPU 0                                                         Core 1 (worker): 6 on CPU 1                                                   Core 0 (main): 7 on CPU 0                                                         Core 1 (worker): 7 on CPU 1                                                       Unpinned worker: 5 on CPU 0                                                 Core 0 (main): 8 on CPU 0                                                         Core 1 (worker): 8 on CPU 1                                                       Unpinned worker: 6 on CPU 0                                                 Core 0 (main): 9 on CPU 0                                                         Core 1 (worker): 9 on CPU 1                                                       Unpinned worker: 7 on CPU 0                                                                                                                                 Main unpinned, affinity: -1                                                     Unpinned main on CPU 0                                                            Core 1 worker finished                                                        Unpinned main on CPU 0                                                              Unpinned worker: 8 on CPU 0                                                 Unpinned main on CPU 0                                                              Unpinned worker: 9 on CPU 0                                                 Unpinned main on CPU 0                                                          Unpinned main on CPU 1                                                              Unpinned worker finished                                                                                                                                    Example complte!

- 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.
@eliasnaur
Copy link
Contributor

Do you actually care about the particular core? If not, are the existingruntime.LockOSThread andruntime.UnlockOSThread calls sufficient to lock/unlock a goroutine to a core?

@amken3d
Copy link
Author

amken3d commentedNov 17, 2025
edited
Loading

This is what I see for LockOsThread

// LockOSThread wires the calling goroutine to its current operating system thread.
// Stub for now
// Called by go1.18 standard library on windows, seegolang/go#49320
func LockOSThread() {
}

// UnlockOSThread undoes an earlier call to LockOSThread.
// Stub for now
func UnlockOSThread() {
}

There seems to be no implementation behind it.

For the RP2, since it is symmetrical multi processor, it probably doesn't matter which exact core. But for something like StM32h7, it would matter which core you pin to. (I know we don't support multicore on it yet)

@eliasnaur
Copy link
Contributor

I know. What I'm saying is to changeLockOSThread to mean "lock the current goroutine to a core" (when using thecores scheduler).

@amken3d
Copy link
Author

amken3d commentedNov 17, 2025
edited
Loading

Fair point. That seems reasonable to me. I can use those function names instead.
The only issue I see is that Lock OsThread can't take any arguments. I'd like to be able to tell which core to lock to.

@eliasnaur
Copy link
Contributor

Right. SoLockOSThread is enough for use cases where you only care about exclusive access to a some core. For heterogeneous cores, I suggest:

  • Move the API to packagemachine.
  • DropCurrentCPU - it's racy (its result may be invalidated at any time).
  • DropGetAffinity - it seems that code that pin itself to a particular core should know what it's doing.
  • Drop the-1 special case fromLockToCore
  • RenameLockToCore toLockCore to mimicLockOSThread naming. RenameUnlockFromCore toUnlockCore for the same reason.

An important issue to think about is what happens if the requested core is busy?LockOSThread doesn't have this problem (some core must be running it).

- Dropped CurrentCPU- Dropped GetAffinity- Renamed LockToCore to LockCore to mimic LockOSThread naming.- Updated examples program
@amken3d
Copy link
Author

Looks like it passed all checks except the macos(13) test with this error

This is a scheduled macos-13 brownout. The macOS-13 based runner images are being deprecated. For more details, seeactions/runner-images#13046.

@deadprogram
Copy link
Member

@amken3d I just created#5093 to address the macOS 13 runner deprecation.

Copy link
Contributor

@eliasnaureliasnaur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Thanks. I've commented on the implementation, but I'm still not a fan of theLockCore API, because it may block indefinitely if a long-running goroutine is running on the target core. In a sense,LockCore acts as a per-core mutex that some arbitrary other goroutine may have taken, with the usual deadlock risks.

One way of getting around this issue is by requiringLockCore to be called before any other goroutine has started. A good place would be in aninit function.

// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

While here, remove this now irrelevant comment.

Comment on lines +101 to +102
// On microcontrollers with multiple cores (e.g., RP2040/RP2350), this pins the
// goroutine to the core it's currently running on.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Is it more precise to say "with the "cores" scheduler"?


constnumCPU=2// RP2040 and RP2350 both have 2 cores

// LockCore implementation for the cores scheduler.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This needs a more detailed description. For example, it doesn't say what happens if the target core is busy. I believeLockCore returns. If so, this is surprising to me; I would expect that onceLockCore returns, the calling goroutine is running on the target core.

…behavior, and limitations with the "cores" scheduler. Updated LockOSThread and UnlockOSThread comments to reflect core pinning behavior on RP2040/RP2350.
Copy link
Contributor

@eliasnaureliasnaur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

@aykevl should probably take a look.

// 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Superfluous comment.

Comment on lines +16 to +18
// 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I think this should be a hard requirements; that is,LockCore should panic if any other goroutine has started.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Regarding "panic if goroutines started" - I have a specific use case for dynamic pinning:

Motion control board where:

Core 0: Communications and non-critical tasks
Core 1: Hard real-time step generation (must not be interrupted)
The pattern I need is:

func main() {
go func() {
machine.LockCore(1) // Pin worker to core 1
stepGenerationLoop()
}()
machine.LockCore(0) // Pin main to core 0
commsLoop()
}
If LockCore panics when goroutines have started, this pattern wouldn't work.

Would you accept one of these:

  • Adding Gosched() in LockCore (so it actually migrates before returning)
  • Keeping dynamic pinning allowed, with clear documentation of risks
  • Perhaps a convention that each core should only have ONE pinned goroutine?

The deadlock risk is manageable if users follow the pattern of pinning early and using one goroutine per core for pinned work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Adding Gosched() in LockCore (so it actually migrates before returning)

Yes, if we do thisLockCore should not return before the core is pinned.

However, given your use case: is it possible to run the step generation off a hardware timer and an interrupt handler? That seems a better fit, and you can keep both cores running non-critical code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

No, That is precisely the implementation that I am trying to get away from.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Can you elaborate why? Assuming yourstepGenerationLoop sometimes sleeps, it seems much less efficient to lock a core 100% of the time, even during sleeps than running an interrupt off a timer.

Copy link
Contributor

@eliasnaureliasnaurNov 23, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I see. I believe I'm doing something similar on a PIO-capable chip (rp2350). My solution is a 3-layer architecture:

  • Regular Go code generates high-level primitives (line segments, bezier curves etc.) and sends them over a channel. This code is only timing sensitive to the extent that it needs to run often enough to not starve the interrupt handler. Depending on your application, you can get rid of the timing requirement by generating batches of primitives where each batch ends with the machine at a safe point (zero velocity/acceleration etc.).
  • Interrupt handler receives from the channel and chops the primitives into fixed-duration updates and packs them into DMA buffers. For example, 2 steppers is 4 bits (2xdir, 2xstep) per update, and 8 updates per 32-bit PIO FIFO word.
    The interrupt handler is only timing sensitive insofar it must run often enough to fill the DMA buffers. If you don't have too much going on, you may not even need DMA and can get away with feeding the PIO FIFO buffer (8 words I believe) directly.
  • A PIO state machine receives updates from its FIFO (fed directly or through DMA) and multiplex them onto the corresponding GPIO pins. The nature of PIOs makes the timing very robust and decoupled from the activity of the system, except for contention on the memory bus.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

There is always multiple ways to skin a cat. I am using an approach similar to what you described as well. But having core pinning has its advantages as well. Also, It seems like several people have asked for it previously. Appreciate your reviews and responses

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

As one of the other people that have asked for pinning to cores, I'll chime in with my use case: I want to be able to dedicate a RP2040 (or 2350) core to realtime audio sample generation for agroovebox project. We are currently able to do this without issues using the RPI C SDK but the lack of core pinning prevents any attempt at trying to use tinyGo in the future for this or similar applications.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

@maks your use case needsLockOSThread (lock to any core), not the more specificLockCore, right?

Copy link

@maksmaksDec 3, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

@eliasnaur sorry for slow reply. I'm new to Go so I'm unsure of how LockOSThread() is supposed to work for this context, but what I was after was to be able to run a Go routine on a specific core of a RP2040/2350 (eg. core1) and then have all other routines including main run on core0 only.

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

@eliasnaureliasnaureliasnaur left review comments

+1 more reviewer

@maksmaksmaks left review comments

Reviewers whose approvals may not affect merge requirements

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

4 participants

@amken3d@eliasnaur@deadprogram@maks

[8]ページ先頭

©2009-2025 Movatter.jp