Lesson 10.5 · 8 min
Chapter review: streaming replies
Chatterbox v1.1 shows an answer as the model writes it, and Ctrl-C stops a long one without losing the conversation. This last chapter review mixes everything that made that work: futures, tasks, channels, bytes cut in awkward places and select!. Eight predictions, then one exercise that puts the whole streaming path in a single function.
Adds to ChatterboxNothing new: a mixed quiz on the whole chapter, and a bigger exercise that uses all of it.
What this chapter gave Chatterbox
Four lessons turned a program that froze while it waited into one that waits for several things at once. The ideas, one line each:
- Lesson 10.1: an
async fnhands back a future that does nothing until it is awaited; tokio runs futures,tokio::join!drives several at once, and code inside async must never block. - Lesson 10.2:
tokio::spawn(async move { … })starts a task that owns what it uses, and a channel carries its pieces:recvgivesNoneonce every Sender is gone, andsendfails once the Receiver is gone. - Lesson 10.3: the network delivers chunks of bytes, not lines, so bytes wait in a
Vec<u8>until a newline makes a whole line, and only then become text. - Lesson 10.4:
tokio::select!waits for whichever comes first and drops the rest, Ctrl-C becomes a future, and dropping a Receiver is all it takes to stop the task feeding it.
Nothing new appears on this page. If a question stumps you, its answer names the lesson to reread.
Mixed quiz
Eight small programs, each run for real when the course was built. They hop between the four lessons on purpose, so the first job is working out which idea a program is about.
What does this program print?
bye, Sam!saving 3 messages bye, Sam!bye, Sam! saving 3 messages
Reveal the answer
The program prints: bye, Sam! Calling save only makes a future, a recipe for the work, and nothing ever awaits it, so it is thrown away uncooked and saving never prints. A forgotten .await is not an error; the work silently never happens. Lesson 10.1 has the recipe-and-meal picture.
Does it compile?
- It compiles and runs
- Compile error E0373
- Compile error E0382
Reveal the answer
It does not compile: E0373. A spawned task may run on after the code that started it is gone, so tokio will not let it borrow anything. Here the block only borrows banner, and the compiler says the async block may outlive the function. async move hands the task its own banner and it compiles (lesson 10.2).
error[E0373]: async block may outlive the current function, but it borrows `banner`, which is owned by the current function --> src/main.rs:4:29 | 4 | let task = tokio::spawn(async { | ^^^^^ may outlive borrowed value `banner` 5 | println!("{banner}"); | ------ `banner` is borrowed here | = note: async blocks are not executed immediately and must either take a reference or ownership of outside variables they use help: to force the async block to take ownership of `banner` (and any other referenced variables), use the `move` keyword | 4 | let task = tokio::spawn(async move { | ++++
Three chunks arrive. What does this print?
data lines: 2, waiting: 0 bytesdata lines: 3, waiting: 0 bytesdata lines: 1, waiting: 12 bytes
Reveal the answer
The program prints: data lines: 2, waiting: 0 bytes Three newlines make three lines: data: Hi, a blank line, and data: [DONE], whose two halves were glued back together in pending before anyone read them. The blank line is not a data line, so the count is 2, and nothing is left waiting. Lines come from newlines, never from chunks (lesson 10.3).
Does it compile?
- It compiles and runs
- Compile error E0728
- Compile error E0308
Reveal the answer
It does not compile: E0728. .await means "pause here", and only async code has somewhere to pause to. reply_twice is a plain fn, so the compiler refuses, the same error as a plain main in lesson 10.1. Make reply_twice async too, and await it in main.
error[E0728]: `await` is only allowed inside `async` functions and blocks --> src/main.rs:9:40 | 8 | fn reply_twice(message: &str) -> String { | --------------------------------------- this is not `async` 9 | let first = pretend_reply(message).await; | ^^^^^ only allowed inside `async` functions and blocks
The answer arrives after 200 ms. What does this print?
stoppedgot Some("Hello")got None
Reveal the answer
The program prints: stopped With its only Sender dropped, stop.recv() can never get a press, so it finishes at once with None. select! takes whichever arm finishes first, and the pattern _ accepts None too, so it prints stopped although nobody pressed anything. That is why the tests in lesson 10.4 keep a _press alive.
The slow answer takes 300 ms, the fast one 50 ms. What is the LAST line this program prints?
slow answer, then fast answerfast answer, then slow answerslow done
Reveal the answer
The program prints: slow answer, then fast answer Both waits run at once, so fast done prints first and slow done second. But tokio::join! hands back its results in the order you wrote the futures, not the order they finished, so a is the slow answer (lesson 10.1).
It compiles. What happens when it runs?
- It compiles and runs
- It compiles, then panics
- Compile error E0728
Reveal the answer
It compiles, then panics at run time. blocking_send waits the plain way, which is right on the keyboard thread of lesson 10.4 and wrong inside async code, where waiting the plain way would hold up every other task on that thread. The compiler cannot see the difference, so tokio checks while the program runs and panics. Inside async, use send(...).await.
What does this program print?
2 pieces, then the channel closed1 pieces, then the channel closed0 pieces, then the channel closed
Reveal the answer
The program prints: 2 pieces, then the channel closed Cloning a Sender gives the pipe a second way in; that is the multiple producer in mpsc. The channel closes only when every Sender is gone, so the first task finishing does not end the loop. Both items arrive, then recv gives None (lesson 10.2).
Milestone: the streaming path in one function
In Chatterbox the stream crosses three places: the real model's task reads chunks, turns whole lines into pieces and sends them down a channel, and answer() races those pieces against Ctrl-C. Here all of it sits in one function, relay, so a test can feed it chunks and press its stop button. parse_event from lesson 10.3 is already written; the rest is yours.
The exercise runs on the Rust Playground, which has tokio and serde. The hidden tests never wait on a clock: every step is a channel handing something over, so they give the same answer on any machine.
Write two bodies. whole_lines takes every finished line out of the waiting bytes and leaves the unfinished rest behind. relay loops on a tokio::select! between the next chunk and a press on stop: add each chunk to its own waiting bytes, read whole lines with parse_event, send each Piece to out, and return the right Ending. The hidden tests cut a letter in half, end a stream with and without [DONE], press stop before and during an answer, and walk away from out.
Write two bodies. whole_lines takes every finished line out of the waiting bytes and leaves the unfinished rest behind. relay loops on a tokio::select! between the next chunk and a press on stop: add each chunk to its own waiting bytes, read whole lines with parse_event, send each Piece to out, and return the right Ending. The hidden tests cut a letter in half, end a stream with and without [DONE], press stop before and during an answer, and walk away from out.
Hidden tests (8): whole_lines_never_breaks_a_letter whole_lines_keeps_an_unfinished_line nobody_listening_is_gone stop_before_anything_arrives a_letter_cut_in_half_arrives_whole a_stream_that_just_ends_is_closed pieces_arrive_in_order_and_done_ends_it stopping_keeps_what_was_shown_and_lets_the_feed_go
This exercise runs real Rust in your browser through the official Rust Playground, so it needs JavaScript.
The end of the course
That was the last page. You started with "what is a program" and finished with a function that reads a stream cut in random places, keeps every letter whole, and stops cleanly when told to, all of which you can now explain line by line. Pick one of the next steps at the end of lesson 10.4 (a /clear command or a timeout arm is a good first one), then keep going with the tokio tutorial and the Rust Book. Thank you for building Chatterbox.
Say it out loud, then reveal
What happens to an <code>async fn</code> you call but never await?Reveal
Why does <code>tokio::spawn</code> need an <code>async move</code> block?Reveal
move hands it its own values; without it the compiler stops with E0373.Why does a streaming reader keep bytes in a <code>Vec<u8></code> instead of turning each chunk into text?Reveal
Two arms of a <code>tokio::select!</code>: what happens to the one that does not finish first?Reveal
recv() nothing is lost: an item on its way stays in the channel for the next call.How does stopping an answer stop the task that was producing it?Reveal
send returns an error, and it returns, dropping everything it owned.Your Chatterbox so far: v1.1 (after lesson 10.4)
Download this version (zip)Unzip, then cargo run inside the folder.
That is lesson 10.5. Tick it off so the course remembers where you are. That is also the end of chapter 10: a few minutes with the course flashcards now makes it stick, and the chapter’s cheat sheet has every idea on one screen.
Go deeper
Questions people ask
Why does nothing happen when I call an async function in Rust?
Calling an async function only creates a future. The work runs when you await it with .await inside async code, or hand it to a runtime such as tokio. A future that is never awaited is dropped without running.
Can I use blocking_send inside async code?
No. tokio panics if blocking_send is called from async code, because waiting the plain way would hold up every other task on that thread. Use send with .await inside async code, and keep blocking_send for an ordinary thread.
Why did my select! take the stop branch when nobody pressed stop?
If every Sender of the stop channel has been dropped, recv returns None at once, and an arm whose pattern is an underscore accepts that. Keep a Sender alive for as long as the stop button should work.
What should I learn after this Rust course?
Extend Chatterbox first: a clear command, a system message, more tests or a timeout. Then the tokio tutorial goes deeper into tasks, channels and select, and the free Rust Book covers the language from another side.
