Threads

Thread Sleep and Yield

The std::thread module provides four functions to pause execution, cooperate with the OS scheduler, or wait for a wakeup notification on the current thread: thread::sleep, thread::yield_now, thread::park, and thread::park_timeout.

Comparison of Thread Suspension Functions

Function Signature OS Scheduler Behavior Primary Use Case
thread::sleep(dur) fn sleep(dur: Duration) Suspends the current thread for at least dur (consumes 0 CPU cycles while sleeping) Rate-limiting, retry backoff, periodic polling
thread::yield_now() fn yield_now() Voluntarily gives up the remainder of the thread's current CPU time slice to other runnable threads Contended spin-loops after std::hint::spin_loop()
thread::park() fn park() Blocks the current thread until its Thread::unpark() token is made available (or returns immediately if already unparked) Custom synchronization primitives, work-stealing idle workers
thread::park_timeout(dur) fn park_timeout(dur: Duration) Blocks until unparked or until dur elapses Bounded-wait custom condition polling
Thread State Transitions in the OS Scheduler:

  [ Running on CPU Core ]
     |
     +-- thread::yield_now() ------> [ Runnable Queue ] (Ready to be scheduled again immediately)
     |
     +-- thread::sleep(Duration) --> [ Sleeping / Blocked ] --(Timer expires)--> [ Runnable Queue ]
     |
     +-- thread::park() -----------> [ Parked / Blocked ] --(other calls .unpark())--> [ Runnable ]

Using thread::sleep, thread::yield_now, and std::hint::spin_loop

When waiting for a lock or condition in a low-level lock-free structure:

  1. For the first few iterations (nanoseconds), call std::hint::spin_loop(), which emits a CPU pause instruction (pause on x86_64, yield on AArch64) to reduce pipeline power consumption without invoking an OS context switch.
  2. If still contended, call std::thread::yield_now() to yield the OS time slice.
  3. For longer waits, block via thread::park(), a Condvar, or thread::sleep(Duration) so the thread consumes zero CPU cycles:
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
use std::time::{Duration, Instant};

fn main() {
    let start = Instant::now();
    let progress = AtomicUsize::new(0);

    thread::scope(|s| {
        s.spawn(|| {
            thread::sleep(Duration::from_millis(5));
            progress.store(100, Ordering::Release);
        });

        // Wait for `progress == 100` using polite yielding:
        let mut spins = 0_u32;
        while progress.load(Ordering::Acquire) < 100 {
            if spins < 10 {
                std::hint::spin_loop();
                spins += 1;
            } else {
                thread::yield_now();
            }
        }
    });

    println!(
        "completed after {:?} (progress = {})",
        start.elapsed(),
        progress.load(Ordering::Relaxed)
    );
}
completed after 5.21ms (progress = 100)

Important Caveat: Never Call std::thread::sleep Inside an async fn!

std::thread::sleep blocks the entire underlying OS worker thread, freezing all other concurrent async tasks multiplexed onto that thread by the async runtime. In async Rust, always use the runtime's non-blocking timer (tokio::time::sleep(dur).await).