Skip to main content

spawn_dedicated_thread

Function spawn_dedicated_thread 

Source
pub fn spawn_dedicated_thread<F>(
    name: String,
    priority: ThreadPriority,
    stack: StackSizeClass,
    f: F,
) -> Result<JoinHandle<()>>
where F: FnOnce() + Send + 'static,
Expand description

Spawn a dedicated OS thread that runs f at priority with a stack of StackSizeClass, plus whatever ambient async context block_on_sync needs on this target.

§Why the stack class is a parameter and not a default

C creates an IOC thread with epicsThreadCreate(name, priority, stackSize, fn, arg) — three attributes. This seam carried the first two and let the third fall through to whatever std picks, which is 2 MiB on RTEMS: std/src/sys/thread/unix.rs gates its DEFAULT_MIN_STACK_SIZE on not(any(l4re, vxworks, espidf, nuttx)), and vxWorks got a 256 KiB carve-out where RTEMS did not.

That is invisible on the host, where a thread stack is lazily-committed virtual address space, and decisive on the target, where it is carved eagerly out of a fixed pool. Making it a parameter is what stops a new per-connection thread from silently costing 2 MiB: there is no default to inherit, so every caller states what the thread is for.

Not spawn_blocking_with_priority, and the difference is the point. That one hands the closure to a pool: tokio’s blocking pool on the host, and on RTEMS a shared callback-pool worker that also drops the priority (see its exec_backend arm). Both are right for work that finishes. A server thread that lives as long as its connection would occupy a pool worker for that whole time, so the pool is the wrong home for it — an IOC thread that a C IOC would create with epicsThreadCreate wants a thread of its own, and the priority a C IOC gives it.

§Why the ambient context is part of this, and not the caller’s problem

block_on_sync picks its mechanism from the thread it is called on, but it cannot create the context a future needs. On the host a fresh std::thread has no runtime, so a future that spawns tasks or arms timers panics with “there is no reactor running” the moment it is polled — even though block_on_sync itself was perfectly happy to park. On RTEMS the exec backend is process-global (background_init), so a bare thread is already complete. That asymmetry is a property of the two backends, so it is resolved here, at the seam, rather than by a cfg in every server that wants a thread.

The captured context is whatever the calling thread is running under, so call this from the runtime the work should belong to. When there is none (RTEMS always; on the host a caller that is itself outside a runtime) the thread simply runs without one, which is exactly right for a future whose awaits are all runtime-agnostic.

§A current-thread ambient is not inherited, and that is the rule

RuntimeHandle::try_current answers two different questions with one value: “am I running on this runtime’s thread” and “has this thread merely entered this handle”. block_on_sync cannot distinguish them, so it must assume the first and refuse to park under a CurrentThread flavor — correct on that runtime’s own thread, where parking halts the task that would wake you, and wrong on a dedicated thread, where it halts nothing.

So the dual meaning is removed here, at the one place a dedicated thread’s context is decided, rather than left for block_on_sync to guess: a CurrentThread ambient is not inherited, and the thread runs with no runtime — the park_on arm, which is sound for it and is the only arm RTEMS ever takes.

Nothing is lost by declining it. What inheriting buys is stated above — spawn and the timer inside block_on_sync — and under a CurrentThread ambient block_on_sync returns Err(CurrentThreadRuntime), so every one of those powers is unreachable anyway. Inheriting it can only convert a thread that would have worked into one that cannot block at all. Measured as exactly that: the PVA client’s blocking byte pumps (runtime::blocking_io::spawn_pump) are dedicated threads whose bodies are pure tokio::sync channel traffic, and every #[tokio::test] that drives one is CurrentThread by default — inheritance made the reader pump exit on its first chunk and the connection read as “server closed during handshake”.