---
title: "23.x Chapter 23 summary and quiz"
description: "Review of when to use async, async/await and Futures, the Tokio runtime, and the main async pitfalls, with a conceptual exercise."
url: "https://learnrust.net/chapter-23/chapter-23-summary-and-quiz/"
last_updated: "2026-06-13"
---

# 23.x Chapter 23 summary and quiz

Async is the course's most specialized chapter, and the one most honest about its costs. Review, then a conceptual exercise.

## Quick review

Async is for **IO-bound** work, many tasks that mostly *wait* (network, disk, timers) ([23.1](https://learnrust.net/chapter-23/what-async-is/)). CPU-bound work wants threads (chapter 22); async wants **tasks**, lightweight units far cheaper than threads, so a few threads can juggle tens of thousands of waiting tasks. Don't use async for CPU-bound work, simple CLI tools, or low concurrency; it's a specialized tool, not a performance upgrade.

An **`async fn`** returns a lazy **`Future`** ([23.2](https://learnrust.net/chapter-23/async-await-and-futures/)): calling it runs *nothing* until the future is driven. **`.await`** drives a future and yields its value, and can only be used inside an `async` function, so async "colors" the call chain upward. The chain bottoms out at a **runtime**, which Rust deliberately doesn't ship, so async code does nothing until you add one.

**Tokio** is the standard runtime ([23.3](https://learnrust.net/chapter-23/using-tokio/)): `#[tokio::main]` turns `async fn main` into a real `main` that runs the runtime; `tokio::spawn` creates concurrent tasks; `tokio::time::sleep(...).await` waits *without* blocking, so many waits overlap (two 100ms sleeps finish in ~100ms). Use async equivalents, never blocking `std` calls, inside tasks.

The **pitfalls** ([23.4](https://learnrust.net/chapter-23/async-pitfalls/)): blocking the executor (a blocking call freezes all tasks on the thread, fix with async versions or `spawn_blocking`); non-`Send` values held across `.await` (futures must be `Send` to spawn, the chapter-22 rule again, fix with `Arc` or drop-before-await); and cancellation at await points (code after an await may never run, put must-run cleanup in `Drop`).

## Quiz time

**Question #1**

A program resizes 10,000 images as fast as possible. Async or threads? Why?

<details class="solution">
<summary>Show solution</summary>

Threads (or `rayon`). Image resizing is CPU-bound, the bottleneck is computation, not waiting, so the way to go faster is more cores working at once. Async helps only with IO-bound *waiting*, of which there's none here; making this async would add complexity and a runtime dependency for zero benefit. Use `rayon`'s `par_iter` or a thread pool.

</details>

**Question #2**

What does this print, and why?

```rust
async fn greet() { println!("hello"); }

fn main() {
    let _f = greet();
    println!("done");
}
```

<details class="solution">
<summary>Show solution</summary>

It prints only `done` (and the compiler warns the future is unused). Calling `greet()` builds a lazy `Future` but doesn't run its body, and nothing drives it: `main` isn't async so it can't `.await`, and there's no runtime. So `hello` never prints. To run it you'd need a runtime (e.g. `#[tokio::main] async fn main`) and to `.await` the future.

</details>

**Question #3**

Why does `tokio::spawn` require its future to be `Send`, and how does that connect to chapter 22?

<details class="solution">
<summary>Show solution</summary>

Tokio runs tasks across multiple threads and may move a task between them, so a spawned future must be safe to send between threads, i.e. `Send` (chapter 22's marker trait). A future captures any value held across its `.await` points, so holding a non-`Send` value (like `Rc`) across an await makes the whole future non-`Send` and unspawnable. It's the identical `Send` rule from chapter 22, applied to futures; the fix is the same (use `Arc`, or drop the value before the await).

</details>

**Question #4**

Why should must-run cleanup go in a `Drop` implementation rather than in code after an `.await`?

<details class="solution">
<summary>Show solution</summary>

Because async tasks can be *cancelled* at an `.await` point: if the future is dropped while suspended there, it stops and any code after that await never runs, so cleanup written after an await can be skipped. A value's `Drop` runs even when the future is cancelled and dropped, so RAII-style cleanup (chapter 21) survives cancellation. Put guaranteed cleanup in `Drop`, not in post-await statements.

</details>

## Chapter exercise

No code to run this time, a design question, since the lesson is about *judgment*. For each scenario, say whether you'd use async, threads/`rayon`, or neither (plain sequential code), and why:

1. A command-line tool that reads one CSV file, sums a column, and prints the total.
2. A chat server holding 50,000 simultaneous client connections, mostly idle.
3. A program that computes SHA-256 hashes of 100,000 files on local disk as fast as possible.

<details class="solution">
<summary>Show solution</summary>

1. **Neither, plain sequential code.** It does one quick read and one computation; there's no concurrency to exploit and nothing to overlap. Async would add a runtime and complexity for nothing; threads would add coordination for nothing. Keep it simple.

2. **Async.** This is the textbook case: a huge number of concurrent, IO-bound, mostly-*waiting* connections. A thread per connection (50,000 threads) would waste gigabytes of stacks and crush the scheduler; lightweight async tasks let a few threads juggle them all, since most are idle at any moment. This is exactly what async was built for.

3. **Threads / `rayon`.** Hashing is CPU-bound (the work is computation, not waiting), and the files are independent, so it parallelizes cleanly across cores. `files.par_iter().map(hash).collect()` with `rayon` (chapter 22) is the natural fit. (There's *some* disk IO, but the bottleneck is the hashing; if it were pure IO you'd reconsider, but "as fast as possible" on local files points at CPU parallelism.)

The meta-point: the right concurrency tool follows from the bottleneck. Waiting at scale → async; computing in parallel → threads/`rayon`; neither → sequential. Most programs are the third case, and reaching for concurrency they don't need is its own mistake.

</details>

You've now seen all three concurrency stories Rust offers, threads, parallelism, and async, and, just as importantly, when to use none of them. Chapter 24 shifts to a different kind of capability entirely: macros, code that writes code at compile time. It opens by finally answering a question you've carried since the very first program: what that `!` after `println` actually means.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
