Mojo · Busy Waiting

Busy Waiting

4.0.0-rc.1 · d927445f

Here’s what “busy waiting” looks like in C++:

while (!has_work_to_do())
    std::this_thread::yield();

On Linux, the std::this_thread::yield() translates into a sched_yield system call, which means context switching to the kernel and back. Instead, you can replace the standard_yield_t wrapper with a platform-specific micro-wait instruction, which is much cheaper. Those instructions, like WFET on Arm, generally hint the CPU to transition to a low-power state.

WrapperISAInstructionPrivileges
x86_pause_tx86PAUSER3
x86_tpause_tx86+WAITPKGUMONITOR+UMWAITR3
arm64_yield_tAArch64YIELDEL0
arm64_wfe_tAArch64LDAXR+WFEEL0
arm64_wfet_tAArch64+WFXTWFETEL0
riscv64_pause_tRISC-VPAUSEU
riscv64_wrs_tRISC-V+ZawrsWRS.STO/WRS.NTOU

No kernel calls. No futexes. Works in tight loops.

The UMWAIT, WFE, WFET, and WRS wrappers go a step further than a spin hint: they sleep the core until a store to the watched line, capped by a timeout only where the loop guards more than one word, so the pool parks a worker in a low-power state rather than burning the core. spin_mutex<fu::preferred_yield_t> takes the same wrappers, while the default spin_mutex_t yields to the scheduler and Rust’s SpinMutex spins on core::hint::spin_loop - a futex-free std::mutex alternative, and the lock used in the NUMA example above. On WebAssembly threads the waiter is wasm_wait_t: a 32- or 64-bit watch sleeps in memory.atomic.wait until a wasm_atomic_ref releasing write notifies or 1 µs passes, so pair the two; uncapped waits and 1- or 2-byte watches spin.