Three lookups, awaited one after the other:
let name = load_name(id).await;
let email = load_email(id).await;
let posts = load_posts(id).await;If each takes 40ms this takes 120ms, and for 80 of those 120 milliseconds the program is sitting on its hands. The three lookups do not depend on each other, so they should be waiting at the same time.
tokio::join! does exactly that:
let (name, email, posts) = tokio::join!(
load_name(id),
load_email(id),
load_posts(id),
);Now the call takes 40ms. join! takes the futures, polls each one whenever it can make progress, and returns a tuple of their outputs in the order you listed them, once all of them are done.
join! is not spawntokio::spawn hands a future to the runtime as an independent task. join! keeps everything on the current task and just interleaves the polling. That has real consequences:
Send or 'static. They can borrow local variables.JoinHandle and no Result to unwrap.join! will not use two cores.Reach for join! when you have a fixed, known set of things to wait on. Reach for spawn when the work should outlive the current scope or genuinely needs its own thread.
try_join!join! waits for everything, even after one branch has already returned an Err. When every branch returns a Result and one failure makes the rest pointless, use try_join!:
let (a, b) = tokio::try_join!(load_a(), load_b())?;It returns Ok((a, b)) when both succeeded, or the first Err it sees, dropping the other future mid-flight. Both branches must have the same error type.
Five lookup functions are already written for you: load_name, load_email, load_posts, load_quota and load_rate_limit. Each one sleeps for 40ms. Do not change them.
load_profilepub async fn load_profile(id: u32) -> ProfileRun load_name, load_email and load_posts concurrently and put the three results into a Profile. The call must take about 40ms, not 120ms.
load_limitspub async fn load_limits(
id: u32,
) -> Result<(u32, u32), String>Run load_quota and load_rate_limit concurrently. Return Ok((quota, rate_limit)) if both succeed, or the error if either fails.
assert_eq!(load_limits(3).await, Ok((300, 60)));
assert_eq!(
load_limits(0).await,
Err("no quota for user 0".to_string())
);spawn anything. Both functions are a single macro call plus a little plumbing.join! needs to own them.let (name, email, posts) = tokio::join!(...) destructures straight into the three bindings.Profile { name, email, posts } then uses field init shorthand.tokio::try_join!(load_quota(id), load_rate_limit(id)) already has the exact return type load_limits needs, so it can be the last expression in the body..await before the macro.Three lookups, awaited one after the other:
let name = load_name(id).await;
let email = load_email(id).await;
let posts = load_posts(id).await;If each takes 40ms this takes 120ms, and for 80 of those 120 milliseconds the program is sitting on its hands. The three lookups do not depend on each other, so they should be waiting at the same time.
tokio::join! does exactly that:
let (name, email, posts) = tokio::join!(
load_name(id),
load_email(id),
load_posts(id),
);Now the call takes 40ms. join! takes the futures, polls each one whenever it can make progress, and returns a tuple of their outputs in the order you listed them, once all of them are done.
join! is not spawntokio::spawn hands a future to the runtime as an independent task. join! keeps everything on the current task and just interleaves the polling. That has real consequences:
Send or 'static. They can borrow local variables.JoinHandle and no Result to unwrap.join! will not use two cores.Reach for join! when you have a fixed, known set of things to wait on. Reach for spawn when the work should outlive the current scope or genuinely needs its own thread.
try_join!join! waits for everything, even after one branch has already returned an Err. When every branch returns a Result and one failure makes the rest pointless, use try_join!:
let (a, b) = tokio::try_join!(load_a(), load_b())?;It returns Ok((a, b)) when both succeeded, or the first Err it sees, dropping the other future mid-flight. Both branches must have the same error type.
Five lookup functions are already written for you: load_name, load_email, load_posts, load_quota and load_rate_limit. Each one sleeps for 40ms. Do not change them.
load_profilepub async fn load_profile(id: u32) -> ProfileRun load_name, load_email and load_posts concurrently and put the three results into a Profile. The call must take about 40ms, not 120ms.
load_limitspub async fn load_limits(
id: u32,
) -> Result<(u32, u32), String>Run load_quota and load_rate_limit concurrently. Return Ok((quota, rate_limit)) if both succeed, or the error if either fails.
assert_eq!(load_limits(3).await, Ok((300, 60)));
assert_eq!(
load_limits(0).await,
Err("no quota for user 0".to_string())
);spawn anything. Both functions are a single macro call plus a little plumbing.join! needs to own them.let (name, email, posts) = tokio::join!(...) destructures straight into the three bindings.Profile { name, email, posts } then uses field init shorthand.tokio::try_join!(load_quota(id), load_rate_limit(id)) already has the exact return type load_limits needs, so it can be the last expression in the body..await before the macro.