Lesson 10.1 · 14 min
Waiting without freezing
Chatterbox v1.0 can talk to a real model, and while it waits for the answer it is frozen: no first word, no way to stop it, nothing until the whole reply has arrived. Chapter 10 fixes that. This lesson learns the tool first, on small programs: async, .await and a runtime called tokio. Chatterbox itself does not change yet.
Adds to ChatterboxNothing new yet: async, .await and tokio on small programs.
Compiler errors you will meet E0728
Frozen while it waits
In lesson 9.2, Model::reply sends the conversation with reqwest::blocking. Blocking means what it sounds like: the program stops on that line until the whole answer has come back. A model writes its answer a few words at a time, and a long one can take many seconds, but Chatterbox sees none of it until the last word arrives. It cannot print the first words, and it cannot notice you pressing a key, because it is not doing anything. It is waiting.
Real chat apps show the answer as it is written. For that, a program has to wait for several things at once: the next piece of the answer, and you. Rust's way to do that is async code, code that can pause at a wait and let other work run meanwhile. This lesson teaches it on small programs; Chatterbox stays at v1.0 until the next lesson.
A recipe, not a meal
Put async in front of fn and the function changes in one surprising way: calling it does not run it. It hands back a future, a value that describes the work, like a recipe nobody has cooked yet. The work happens when you write .await after the future. .await means "do this now, and if it has to wait, pause here until it is done".
You have met this laziness before. In lesson 6.5, .map(...) on an iterator did nothing until .collect() asked for the results. A future is the same: nothing happens until something asks. (The #[tokio::main] line is explained just below.) Guess the first line this prints:
What is the FIRST line this program prints?
future madegreet startshi, Sam!
Reveal the answer
The program prints: future made greet("Sam") only made the future, so future made comes first. The body of greet, its println! included, runs at later.await, and only then does hi, Sam! come back.
Who does the cooking: tokio
Something has to run futures: keep track of which ones are waiting, and wake each one up when what it waits for is ready. That something is called a runtime. The Rust language gives you async and .await but no runtime, so you pick one from a crate. The most used one is tokio.
The line #[tokio::main] above async fn main() is an attribute, like #[test] in lesson 7.3. It rewrites main so that it starts a tokio runtime and hands it your async main to run. Without it, nobody does the cooking, and the compiler says so. Here is the mistake almost everyone makes first: .await inside a plain main.
This is the exact output of the real compiler. Toggle the English, then pick a fix and see what actually happens.
error[E0728]: `await` is only allowed inside `async` functions and blocks --> src/main.rs:6:29 | 5 | fn main() { | --------- this is not `async` 6 | let text = greet("Sam").await; | ^^^^^ only allowed inside `async` functions and blocks
E0728 in the compiler error atlas →
`await` is only allowed inside `async` functions and blocks:.awaitmeans "pause here", and only async code has somewhere to pause to.this is not `async`points atfn main(): a plain function runs straight through from top to bottom, so it cannot stop halfway and pick up later.- The
greetfunction is fine. The mistake is where its future is awaited, not how it is written. - For
main, addingasyncalone is not enough, because something has to run an asyncmain. That is the trap below.
Fixes that were compiled for real
- Let tokio run an async main — compiles. <code>#[tokio::main]</code> starts the runtime, and <code>async fn main()</code> may now pause at <code>.await</code>.
- Trap: async main with no runtime — still E0752. Marking <code>main</code> async without <code>#[tokio::main]</code> leaves nobody to run it. The compiler refuses with a different error: <code>main</code> is not allowed to be async on its own.
Two waits at once
Now the point of all this. tokio::time::sleep(Duration::from_millis(1000)) makes a future that is ready after 1000 milliseconds, one second. Duration is the standard library's type for a length of time. Instant::now() notes the current moment, so that start.elapsed() can tell how long has passed since, and .as_secs() gives that in whole seconds. Awaiting two sleeps one after the other takes two seconds, as you would expect:
$ cargo run
first answer is ready
second answer is ready
took 2 stokio::join! is different. Like println!, it is a macro (the ! says so). It takes several futures, runs them all at once, waits until every one is done, and hands back their results together as a tuple (lesson 2.6). The same two one-second waits, now joined:
What is the LAST line this program prints?
took 2 stook 1 stook 0 s
Reveal the answer
The program prints: took 1 s Both sleeps start together and finish together, about one second later. An .await is where a future says "I cannot go on yet", and the runtime uses that moment to drive the other one. Neither wait has to finish before the other may start.
No second copy of the program ran. One runtime drove both futures, switching between them at their waits. That is why async is cheap: a future that is waiting costs almost nothing, so one program can have many waits going at once.
The one rule: never block inside async
A future can only step aside at an .await. Code that waits without one never steps aside. std::thread::sleep is the blocking kind of sleep: it stops the whole thread (the worker that runs your code, step by step), and with it every future that thread is driving. Swap it into the joined program and the join stops helping:
$ cargo run
first answer is ready
second answer is ready
took 2 s| Program | First wait | Second wait | Total |
|---|---|---|---|
.await, then .await | 0 to 1 s | 1 to 2 s | 2 s |
tokio::join! with tokio's sleep | 0 to 1 s | 0 to 1 s | 1 s |
tokio::join! with std::thread::sleep | 0 to 1 s | 1 to 2 s | 2 s |
This is Chatterbox's problem in miniature. reqwest::blocking waits the way std::thread::sleep does. reqwest also has an async side, where .send() gives back a future you .await, and the rest of this chapter switches to it.
The trait that will not fit in a box
So the plan for Chatterbox sounds simple: make reply an async fn in the Replier trait. Rust does allow async functions in traits. But Chatterbox keeps its replier as Box<dyn Replier> (lesson 8.4), so that /model can swap the echo, the pretend model and the real one while it runs. Here is a cut-down version of that. Will it still compile?
Does it compile? You do not need to know any error number: the choice is between running, crashing while it runs, and the compiler refusing.
- It compiles and runs
- It compiles, then panics
- Compile error E0038
Reveal the answer
It does not compile: E0038. The compiler refuses: the trait `Replier` is not dyn compatible. Dyn compatible means "can be used behind Box<dyn …>". The note mentions a vtable: the table of methods a Box<dyn Replier> uses to find the right function while the program runs. dyn Replier means "some Replier, decided while the program runs", so every method must be callable without knowing the exact type. An async fn returns a future whose type the compiler invents for each implementor: one for Echo, another for Pretend, each with its own size. Code that holds only the box cannot know which future it would get back, and the note names the cause: method `reply` is `async`.
error[E0038]: the trait `Replier` is not dyn compatible --> src/main.rs:14:26 | 14 | let replier: Box<dyn Replier> = Box::new(Echo); | ^^^^^^^ `Replier` is not dyn compatible | note: for a trait to be dyn compatible it needs to allow building a vtable for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility> --> src/main.rs:2:14 | 1 | trait Replier { | ------- this trait is not dyn compatible... 2 | async fn reply(&self, message: &str) -> String; | ^^^^^ ...because method `reply` is `async` = help: consider moving `reply` to another trait = help: only type `Echo` implements `Replier`; consider using it directly instead.
That is the problem the next lesson solves. It keeps reply a plain function that returns at once, and what it returns is a channel: a pipe the answer arrives through later, piece by piece. Every replier returns the same kind of channel, so Box<dyn Replier> works again, and each word can be printed the moment it comes out of the pipe.
A warm-up, separate from Chatterbox. Fill in pretend_reply: wait ms milliseconds without blocking, then answer you said: … (or say something first for an empty message). Then fill in two_replies so that both replies are worked on at the same time. The hidden tests are #[tokio::test] functions: #[test], but run inside a tokio runtime so they may be async and use .await.
A warm-up, separate from Chatterbox. Fill in pretend_reply: wait ms milliseconds without blocking, then answer you said: … (or say something first for an empty message). Then fill in two_replies so that both replies are worked on at the same time. The hidden tests are #[tokio::test] functions: #[test], but run inside a tokio runtime so they may be async and use .await.
Hidden tests (5): an_empty_message_gets_a_nudge answers_like_the_echo it_really_waits the_wait_steps_aside both_replies_come_back_in_order
This exercise runs real Rust in your browser through the official Rust Playground, so it needs JavaScript.
If you have coded before
Python's asyncio has the same shape: async def is async fn, await is .await, asyncio.run(main()) is the runtime that #[tokio::main] starts, and asyncio.gather plays the part of tokio::join!. Calling an async def function also gives back an object that does nothing until it is awaited. In JavaScript, a promise starts running as soon as it is made.
Say it out loud, then reveal
What does calling an <code>async fn</code> give you, and when does its body run?Reveal
Why does <code>.await</code> inside a plain <code>fn main</code> fail with E0728, and what is the fix?Reveal
.await. Write #[tokio::main] above async fn main() so tokio runs it.Two one-second sleeps inside <code>tokio::join!</code> take about one second. Why does the same program with <code>std::thread::sleep</code> take two?Reveal
.await, so the runtime drives the other future meanwhile. std::thread::sleep blocks the whole thread, so nothing else runs until it ends.Why can a trait with an <code>async fn</code> not be used as <code>Box<dyn Trait></code>?Reveal
Your Chatterbox so far: v1.0 (after lesson 9.3)
Download this version (zip)Unzip, then cargo run inside the folder.
That is lesson 10.1. Tick it off so the course remembers where you are.
Go deeper
Questions people ask
What does async mean in Rust?
An async function can pause while it waits for something, such as a timer or a network reply, and let other work run in the meantime. Calling it gives back a future, which does nothing until it is awaited.
What does .await do in Rust?
It runs a future and, if the future has to wait, pauses the current async function at that point until the future is ready. While it is paused, the runtime can run other futures.
What is tokio?
Tokio is the most widely used async runtime for Rust. Rust has async and await in the language but no built-in runtime; tokio runs futures and provides async timers, input and output, and network tools. You start it by writing #[tokio::main] above async fn main.
What is Rust error E0728?
E0728 means .await was used outside async code, for example in a plain fn main. Mark the function async, and for main also add #[tokio::main] so a runtime runs it.
Can I use std::thread::sleep in async code?
It compiles, but it blocks the whole thread, so every other future on that thread stops too. Use tokio::time::sleep with .await instead.