Rust Yielding & Starvation
Yielding is a future handing control back to the runtime on purpose. A runtime can only switch jobs at an await point.
No yield, no turn: the first future runs to the end
A future can only pause at an await point. Code between two awaits runs straight through. There is no switch to other futures. So a long stretch without an await starves the other futures sharing the task. They cannot run until the stretch finishes or awaits:
// Cargo.toml: tokio = { version = "1", features = ["full"] }, futures = "0.3"
#[tokio::main]
async fn main() {
let a = async {
println!("a: step 1");
println!("a: step 2");
};
let b = async {
println!("b: step 1");
println!("b: step 2");
};
futures::join!(a, b);
}yield_now hands control back on purpose
Use yield_now().await to hand control back to the runtime at once. It does so without sleeping. Other ready futures get a turn. Then yours resumes. The closing program is this lesson's first snippet with yields added โ compare the two outputs:
tokio::task::yield_now().await;
// pause here: other ready futures get a turn,
// then this one resumesA short sleep also yields โ but always waits
Sleeping for a millisecond also works as a crude yield, but it always waits at least that long. yield_now says what you mean, and is faster:
tokio::time::sleep(Duration::from_millis(1)).await;
// this frees the thread too, yet it can never resume sooner
// than the millisecondExample
// Cargo.toml: tokio = { version = "1", features = ["full"] }, futures = "0.3"
#[tokio::main]
async fn main() {
let a = async {
println!("a: step 1");
tokio::task::yield_now().await;
println!("a: step 2");
};
let b = async {
println!("b: step 1");
tokio::task::yield_now().await;
println!("b: step 2");
};
futures::join!(a, b);
}Key points
- This is cooperative multitasking.
- Each future chooses when to share.
- Each future also has the duty not to hold on for too long.
- Yielding suits medium stretches of work.
- Truly heavy CPU work still belongs on a blocking thread (the Blocking lesson), not between yields.
- Break work up only where it helps.
- Yielding on every line adds overhead.
- Measure before you sprinkle yields everywhere.
- Threads are switched by the operating system, ready or not.
- Starving is possible because a future must offer to switch.
In this example, two small futures each print two lines, with a yield_now between them. futures::join! polls them in turn. First a prints step 1. Then its yield hands control back. So b gets its turn. It prints step 1 and yields too. Only then do the second steps print, a before b. Without the yields, a would print both of its lines in its first turn. b would wait with nothing to show. That is the starvation this lesson warns about, in miniature.
Note
Futures switch only when they offer to, and that politeness is what makes futures cheap.
Try it Yourself
Edit the code, then press Run.
Running is simulated ยท Rust Playground
Exercise
A future does one long calculation with no await inside it. What happens to the other futures in the same task?