Threads let you run work on more than one CPU. Async lets you keep thousands of half-finished operations in flight on very few threads. Both are concurrency, but they solve different problems: threads are for CPU work, async is for waiting.
Marking a function async changes what it returns:
async fn greet(name: &str) -> String {
format!("Hello, {name}!")
}The signature says -> String, but the compiler rewrites it to return impl Future<Output = String>. You get the String back by awaiting it:
let greeting = greet("Ada").await;This is the part that surprises people coming from other languages. A JavaScript promise starts running the moment you create it. A Rust future does nothing at all until something polls it:
let fut = greet("Ada"); // no work has happened
let s = fut.await; // now the body runsDrop the future without awaiting it and the body never runs. There is no background thread, no scheduler entry, nothing. A future is a plain value describing work that has not started.
Because futures are inert, .await only works inside another async context, and at the top there has to be a runtime that does the polling. In tests that runtime comes from a macro:
#[tokio::test]
async fn it_greets() {
assert_eq!(greet("Ada").await, "Hello, Ada!");
}#[tokio::test] replaces #[test], builds a small runtime, and drives your async test body to completion. For a binary the equivalent is #[tokio::main] on main.
Write two async functions.
greetpub async fn greet(name: &str) -> StringReturn "Hello, {name}!".
greet_and_countpub async fn greet_and_count(
calls: &AtomicUsize,
name: &str,
) -> StringAdd one to calls, then return the result of awaiting greet(name).
The counter is there so the tests can prove laziness. Building the future must leave calls at zero; only awaiting it may bump the count.
let calls = AtomicUsize::new(0);
let fut = greet_and_count(&calls, "Ada");
assert_eq!(calls.load(Ordering::SeqCst), 0);
assert_eq!(fut.await, "Hello, Ada!");
assert_eq!(calls.load(Ordering::SeqCst), 1);impl Future anywhere. async fn does it for you.greet_and_count should call greet, not rebuild the string itself.Ordering::SeqCst for the counter unless you have a reason not to.format!("Hello, {name}!") is the whole body of greet.calls.fetch_add(1, Ordering::SeqCst); adds one and returns the old value, which you can ignore..await on greet(name), or you will try to return a future where a String is expected.async fn, so if you write the body normally the tests pass.Threads let you run work on more than one CPU. Async lets you keep thousands of half-finished operations in flight on very few threads. Both are concurrency, but they solve different problems: threads are for CPU work, async is for waiting.
Marking a function async changes what it returns:
async fn greet(name: &str) -> String {
format!("Hello, {name}!")
}The signature says -> String, but the compiler rewrites it to return impl Future<Output = String>. You get the String back by awaiting it:
let greeting = greet("Ada").await;This is the part that surprises people coming from other languages. A JavaScript promise starts running the moment you create it. A Rust future does nothing at all until something polls it:
let fut = greet("Ada"); // no work has happened
let s = fut.await; // now the body runsDrop the future without awaiting it and the body never runs. There is no background thread, no scheduler entry, nothing. A future is a plain value describing work that has not started.
Because futures are inert, .await only works inside another async context, and at the top there has to be a runtime that does the polling. In tests that runtime comes from a macro:
#[tokio::test]
async fn it_greets() {
assert_eq!(greet("Ada").await, "Hello, Ada!");
}#[tokio::test] replaces #[test], builds a small runtime, and drives your async test body to completion. For a binary the equivalent is #[tokio::main] on main.
Write two async functions.
greetpub async fn greet(name: &str) -> StringReturn "Hello, {name}!".
greet_and_countpub async fn greet_and_count(
calls: &AtomicUsize,
name: &str,
) -> StringAdd one to calls, then return the result of awaiting greet(name).
The counter is there so the tests can prove laziness. Building the future must leave calls at zero; only awaiting it may bump the count.
let calls = AtomicUsize::new(0);
let fut = greet_and_count(&calls, "Ada");
assert_eq!(calls.load(Ordering::SeqCst), 0);
assert_eq!(fut.await, "Hello, Ada!");
assert_eq!(calls.load(Ordering::SeqCst), 1);impl Future anywhere. async fn does it for you.greet_and_count should call greet, not rebuild the string itself.Ordering::SeqCst for the counter unless you have a reason not to.format!("Hello, {name}!") is the whole body of greet.calls.fetch_add(1, Ordering::SeqCst); adds one and returns the old value, which you can ignore..await on greet(name), or you will try to return a future where a String is expected.async fn, so if you write the body normally the tests pass.