Loading pageā¦
Rust walkthroughs
Loading pageā¦
tokio::task::yield_now differ from std::thread::yield_now in cooperative multitasking?tokio::task::yield_now yields control to the async runtime's task scheduler, allowing other tasks on the same thread to make progress before the current task resumes. std::thread::yield_now yields the entire OS thread to the operating system scheduler, allowing other threads on the same CPU core to run. In cooperative multitasking contexts, tokio::task::yield_now is the correct choice: it operates within the async runtime's model, yielding at the task level rather than the thread level. Using std::thread::yield_now in async code is generally wrongāit yields the whole thread rather than just the current task, potentially blocking other tasks and undermining the runtime's scheduling decisions.
use tokio::task::yield_now;
#[tokio::main]
async fn main() {
println!("Before yield");
yield_now().await;
println!("After yield");
// yield_now() returns a future
// Awaiting it allows other tasks to run
// Control returns after other ready tasks complete their turn
}tokio::task::yield_now is an async function that returns control to the scheduler.
use tokio::task::yield_now;
#[tokio::main]
async fn main() {
// Spawn two tasks on the same thread
let task1 = tokio::spawn(async {
for i in 0..5 {
println!("Task 1: iteration {}", i);
yield_now().await; // Let other tasks run
}
});
let task2 = tokio::spawn(async {
for i in 0..5 {
println!("Task 2: iteration {}", i);
yield_now().await; // Let other tasks run
}
});
task1.await.unwrap();
task2.await.unwrap();
// Output interleaves because each task yields after each iteration
}Yielding allows other ready tasks to make progress cooperatively.
use tokio::task::yield_now;
#[tokio::main]
async fn main() {
let task1 = tokio::spawn(async {
// CPU-bound loop without yielding
// Blocks the thread until complete
let mut sum = 0u64;
for i in 0..1_000_000 {
sum = sum.wrapping_add(i);
}
println!("Task 1 done: {}", sum);
});
let task2 = tokio::spawn(async {
println!("Task 2 waiting...");
// This won't run until task1 finishes
// task1 never yields, so task2 is starved
});
task1.await.unwrap();
task2.await.unwrap();
}Without explicit yields, CPU-bound work blocks other tasks on the same thread.
use tokio::task::yield_now;
#[tokio::main]
async fn main() {
let task1 = tokio::spawn(async {
let mut sum = 0u64;
for i in 0..1_000_000 {
sum = sum.wrapping_add(i);
// Yield periodically to allow other tasks to run
if i % 10_000 == 0 {
yield_now().await;
}
}
println!("Task 1 done: {}", sum);
});
let task2 = tokio::spawn(async {
for j in 0..5 {
println!("Task 2: {}", j);
yield_now().await;
}
});
// Now tasks interleave
// Task 2 can make progress while task 1 runs
task1.await.unwrap();
task2.await.unwrap();
}Periodic yields prevent CPU-bound tasks from starving other tasks.
use std::thread;
fn main() {
// std::thread::yield_now yields the OS thread
// Used in synchronous, pre-emptive threading
thread::scope(|s| {
s.spawn(|| {
for i in 0..5 {
println!("Thread 1: {}", i);
thread::yield_now(); // Yield OS thread
}
});
s.spawn(|| {
for i in 0..5 {
println!("Thread 2: {}", i);
thread::yield_now();
}
});
});
// Threads are pre-emptively scheduled by OS
// yield_now is a hint, not a guarantee
}std::thread::yield_now operates at the OS thread level.
use tokio::task;
use std::thread;
#[tokio::main]
async fn main() {
// WRONG: Using std::thread::yield_now in async code
let task1 = tokio::spawn(async {
for i in 0..5 {
println!("Task 1: {}", i);
thread::yield_now(); // Yields the WHOLE THREAD
}
});
let task2 = tokio::spawn(async {
for i in 0..5 {
println!("Task 2: {}", i);
// This may not run if task1 keeps the thread busy
}
});
// thread::yield_now yields the OS thread
// But on a single-threaded runtime, there's no other thread to run
// The scheduler may just continue running the same task
}std::thread::yield_now doesn't yield at the task levelāit yields the thread.
use tokio::task::yield_now;
#[tokio::main(flavor = "current_thread")] // Single-threaded runtime
async fn main() {
let task1 = tokio::spawn(async {
println!("Task 1: before yield");
yield_now().await;
println!("Task 1: after yield");
});
let task2 = tokio::spawn(async {
println!("Task 2: before yield");
yield_now().await;
println!("Task 2: after yield");
});
// On single-threaded runtime:
// - yield_now allows task switching within the single thread
// - task::yield_now affects task scheduling, not thread scheduling
// - std::thread::yield_now would have no effect (no other threads)
task1.await.unwrap();
task2.await.unwrap();
}On single-threaded runtimes, task::yield_now enables cooperative multitasking within one thread.
use tokio::task::yield_now;
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() {
// On multi-threaded runtime:
// - yield_now allows OTHER TASKS on SAME thread to run
// - Other threads continue executing their tasks independently
let task1 = tokio::spawn(async {
for i in 0..10 {
println!("Task 1: {}", i);
yield_now().await;
}
});
let task2 = tokio::spawn(async {
for i in 0..10 {
println!("Task 2: {}", i);
yield_now().await;
}
});
// Tasks may run on different threads
// yield_now affects only the current thread's task queue
}On multi-threaded runtimes, yield_now affects the current worker thread's task queue.
use tokio::task::yield_now;
#[tokio::main]
async fn main() {
let task1 = tokio::spawn(async {
println!("Task 1 start");
yield_now().await; // Yield to scheduler
println!("Task 1 middle");
yield_now().await; // Yield again
println!("Task 1 end");
});
let task2 = tokio::spawn(async {
println!("Task 2 start");
yield_now().await;
println!("Task 2 middle");
yield_now().await;
println!("Task 2 end");
});
let task3 = tokio::spawn(async {
println!("Task 3 start");
yield_now().await;
println!("Task 3 end");
});
// Possible output order (scheduler-dependent):
// Task 1 start
// Task 2 start
// Task 3 start
// Task 1 middle
// Task 2 middle
// Task 3 end (finished)
// Task 1 end
// Task 2 end
task1.await.unwrap();
task2.await.unwrap();
task3.await.unwrap();
}The scheduler decides which ready task runs after each yield.
use tokio::task::yield_now;
#[tokio::main]
async fn main() {
// Blocking CPU work that should yield
async fn cpu_intensive_work(n: u64) -> u64 {
let mut sum = 0u64;
for i in 0..n {
sum = sum.wrapping_add(i);
// Yield every 100_000 iterations
if i % 100_000 == 0 {
yield_now().await;
}
}
sum
}
let result = cpu_intensive_work(1_000_000).await;
println!("Result: {}", result);
}Long-running CPU work should yield periodically.
use tokio::task::yield_now;
#[tokio::main]
async fn main() {
// Approach 1: Yield during CPU work
async fn with_yield() {
for i in 0..100 {
// Do some work
let _ = i * i;
yield_now().await;
}
}
// Approach 2: Spawn CPU work on blocking thread
async fn with_spawn_blocking() {
tokio::task::spawn_blocking(|| {
for i in 0..100 {
let _ = i * i;
}
}).await.unwrap();
}
// yield_now: cooperative, same thread, must yield manually
// spawn_blocking: moves work to dedicated thread pool
// Use yield_now for:
// - Short CPU bursts that won't block too long
// - Cooperative task interleaving
// Use spawn_blocking for:
// - Longer CPU work or blocking operations
// - Truly blocking I/O
}spawn_blocking moves work to a separate thread pool; yield_now cooperates on the current thread.
use tokio::task::yield_now;
#[tokio::main]
async fn main() {
// tokio's scheduler is work-stealing, not priority-based
// yield_now doesn't set priorities
// However, yielding allows:
// - Other tasks to make progress
// - I/O to be processed
// - Timers to fire
// If you have:
// - High-priority task: complete quickly, don't yield excessively
// - Low-priority background task: yield frequently
let high_priority = tokio::spawn(async {
// Do important work quickly
println!("High priority: processing");
// Don't yield unless necessary
});
let low_priority = tokio::spawn(async {
loop {
// Background work: yield frequently
println!("Low priority: chugging along");
yield_now().await;
}
});
}Yielding is cooperativeāthere's no priority mechanism.
use tokio::task::yield_now;
#[tokio::main]
async fn main() {
// Recursive async without yield can overflow stack
async fn recursive_no_yield(n: u32) -> u32 {
if n == 0 { return 0; }
n + recursive_no_yield(n - 1).await
}
// Recursive async with yield prevents stack overflow
async fn recursive_with_yield(n: u32) -> u32 {
if n == 0 { return 0; }
yield_now().await; // Yield before recursion
n + recursive_with_yield(n - 1).await
}
// yield_now forces the future to be scheduled
// This breaks the synchronous recursion chain
let result = recursive_with_yield(1000).await;
println!("Result: {}", result);
}Yielding in recursion prevents stack overflow by breaking the synchronous chain.
use tokio::task::yield_now;
use std::time::Instant;
#[tokio::main]
async fn main() {
// Measure yield_now overhead
let iterations = 1_000_000;
let start = Instant::now();
for _ in 0..iterations {
yield_now().await;
}
let yield_time = start.elapsed();
println!("{} yields took {:?}", iterations, yield_time);
println!("Per yield: {:?}", yield_time / iterations);
// yield_now is relatively cheap but not free
// It involves:
// - Saving current task state
// - Pushing to scheduler queue
// - Rescheduling
}Yielding has overheadāuse appropriately, not excessively.
use tokio::task::yield_now;
use std::time::Duration;
#[tokio::main]
async fn main() {
// yield_now: yield control, resume ASAP
yield_now().await;
// sleep: yield control, resume after duration
tokio::time::sleep(Duration::from_millis(0)).await;
// Both yield, but with different guarantees:
// - yield_now: scheduler will run other ready tasks, then come back
// - sleep(0): same as yield_now in most implementations
// - sleep(>0): will wait at least that duration
// Use yield_now when you just want to cooperate
// Use sleep when you need to wait for time
}sleep with zero duration is similar to yield_now but with timing semantics.
use tokio::task::yield_now;
#[tokio::main]
async fn main() {
// Cooperative processing loop
async fn process_items(items: Vec<i32>) -> Vec<i32> {
let mut results = Vec::new();
for item in items {
// Process item
results.push(item * 2);
// Yield periodically to prevent blocking
yield_now().await;
}
results
}
// This is often unnecessary for small batches
// Only yield if:
// - Many iterations
// - Each iteration is slow
// - Other tasks need responsiveness
}Only yield when necessary for responsiveness.
use tokio::task::yield_now;
use std::collections::VecDeque;
#[tokio::main]
async fn main() {
// Simulate a cooperative task processor
async fn process_queue(mut queue: VecDeque<String>) {
while let Some(item) = queue.pop_front() {
// Process item (potentially expensive)
println!("Processing: {}", item);
let _ = item.to_uppercase();
// Yield to allow other tasks (like new connections)
yield_now().await;
}
}
let mut queue = VecDeque::new();
for i in 0..100 {
queue.push_back(format!("item-{}", i));
}
// Process queue cooperatively
process_queue(queue).await;
}Server handlers can yield to maintain responsiveness.
use std::thread;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
fn main() {
// std::thread::yield_now is for SYNC code
// Used when:
// - Spinning on a condition
// - Waiting for atomic flag without blocking
let done = Arc::new(AtomicBool::new(false));
let done_clone = Arc::clone(&done);
// Worker thread sets flag
thread::spawn(move || {
// Do work
thread::sleep(std::time::Duration::from_millis(100));
done_clone.store(true, Ordering::Release);
});
// Main thread spins (not ideal, but shows use case)
while !done.load(Ordering::Acquire) {
thread::yield_now(); // Don't busy-spin the CPU
}
println!("Done!");
}std::thread::yield_now is for spin-wait patterns in synchronous code.
| Aspect | tokio::task::yield_now | std::thread::yield_now |
|--------|-------------------------|-------------------------|
| Level | Async task | OS thread |
| Returns | Future (must .await) | () (immediate) |
| Context | Async runtime | Any sync code |
| Effect | Yields to task scheduler | Yields to OS scheduler |
| Cooperative | Yes (task level) | Yes (thread level) |
| Async runtime | Only correct choice | Wrong choice |
| Sync code | Cannot use | Correct choice |
| Single-threaded runtime | Works correctly | No effect |
| Multi-threaded runtime | Affects current worker thread | Affects whole thread |
The fundamental difference is the level of abstraction:
tokio::task::yield_now operates at the async task level. When you call it, the current task yields control back to the runtime's scheduler. Other ready tasks on the same worker thread can then execute. This is cooperative multitasking within the async modelāthe task voluntarily yields, and the scheduler decides what runs next. It's implemented as a future that schedules the current task to run again after other ready tasks have had a turn.
std::thread::yield_now operates at the OS thread level. It tells the operating system "I don't need the CPU right now, let someone else run." The OS scheduler then chooses another thread (on that CPU core) to execute. In a traditional multi-threaded program, this allows cooperative scheduling between threads.
Key insight: Using std::thread::yield_now in async code is almost always wrong. On a single-threaded runtime, there are no other threads to yield toāthe call has no meaningful effect on task scheduling. On a multi-threaded runtime, it yields the entire worker thread, which prevents all tasks on that thread from running, not just the current task. This undermines the runtime's scheduling decisions and can cause performance problems.
When to yield: CPU-bound work in async code should periodically yield to prevent starving other tasks. The pattern is simple: check for yields in long loops or after significant computation chunks. For truly blocking or long CPU work, spawn_blocking is the better choiceāit moves work to a dedicated thread pool designed for that purpose. yield_now is for cooperative interleaving of moderate work on the async runtime.