add async-await exercises

This commit is contained in:
chrischiedo 2026-06-17 17:51:16 +03:00
parent 4bab596677
commit 30c8300074
No known key found for this signature in database
GPG Key ID: F288D7930E7EAE87
12 changed files with 467 additions and 0 deletions

View File

@ -196,6 +196,9 @@ edition = "2024"
# Don't publish the exercises on crates.io! # Don't publish the exercises on crates.io!
publish = false publish = false
[dependencies]
trpl = "0.3.0"
[profile.release] [profile.release]
panic = "abort" panic = "abort"

View File

@ -0,0 +1,15 @@
# Async/Await
Rust's async model lets you write concurrent code that waits for I/O or other
work without blocking a thread. An `async fn` returns a `Future` — a value that
represents work still in progress. The `.await` keyword pauses until that work
finishes.
Futures don't run on their own: an **async runtime** (such as [Tokio](https://tokio.rs))
polls them to completion. Rustlings uses Tokio (via the [`trpl`](https://crates.io/crates/trpl) crate) for these exercises.
## Further information
- [The Rust Programming Language: Async Programming](https://doc.rust-lang.org/book/ch17-00-async-await.html)
- [Async Programming Book](https://rust-lang.github.io/async-book/)
- [Tokio tutorial](https://tokio.rs/tokio/tutorial)

View File

@ -0,0 +1,27 @@
// This program fetches a greeting asynchronously. Async functions return a
// `Future` that must be `.await`ed to get the result. An async runtime like
// Tokio is needed to drive futures to completion.
use std::time::Duration;
async fn fetch_greeting() -> String {
trpl::sleep(Duration::from_millis(10)).await;
String::from("Hello, async world!")
}
fn main() {
trpl::block_on(async {
// TODO: We need to `await` the future returned by `fetch_greeting`.
let greeting = fetch_greeting();
println!("{greeting}");
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fetch_greeting_returns_expected_message() {
trpl::block_on(async { assert_eq!(fetch_greeting().await, "Hello, async world!") });
}
}

View File

@ -0,0 +1,32 @@
// This program runs multiple async operations. The `Future` trait represents an
// asynchronous computation. Multiple futures can be polled concurrently with
// macros like `trpl::join!`.
use std::time::Duration;
async fn compute_value(id: u32) -> u32 {
trpl::sleep(Duration::from_millis(10)).await;
id * 10
}
async fn compute_all() -> Vec<u32> {
// TODO: Use `trpl::join!` to await three futures concurrently.
// Collect the results into a vector, e.g. `[10, 20, 30]`.
todo!()
}
fn main() {
trpl::block_on(async {
let results = compute_all().await;
println!("Computed: {results:?}");
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compute_all_returns_expected_values() {
trpl::block_on(async { assert_eq!(compute_all().await, [10, 20, 30]) });
}
}

View File

@ -0,0 +1,42 @@
// Async runtimes like Tokio can spawn independent tasks that run concurrently
// on the runtime's thread pool. Each spawned task returns a `JoinHandle` that
// can be `.await`ed to get its result — similar to `thread::spawn` and `join`.
use std::time::Duration;
async fn double(n: u32) -> u32 {
trpl::sleep(Duration::from_millis(10)).await;
n * 2
}
async fn double_all(values: &[u32]) -> Vec<u32> {
let mut handles = Vec::new();
for &value in values {
// TODO: Spawn `double(value)` on the Tokio runtime using `trpl::spawn_task`.
// Push the returned `JoinHandle` into `handles`.
todo!();
}
let mut results = Vec::new();
for handle in handles {
// TODO: Await each spawned task and collect its result into `results`.
}
results
}
fn main() {
trpl::block_on(async {
let results = double_all(&[1, 2, 3, 4, 5]).await;
println!("Results: {results:?}");
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compute_all_double_values() {
trpl::block_on(async { assert_eq!(double_all(&[1, 2, 3, 4, 5]).await, [2, 4, 6, 8, 10]) });
}
}

View File

@ -0,0 +1,93 @@
// This exercise demonstrates async message-passing concurrency using a channel.
use std::time::Duration;
struct Queue {
first_half: Vec<String>,
second_half: Vec<String>,
}
impl Queue {
fn new() -> Self {
Self {
first_half: vec![
String::from("winter"),
String::from("is"),
String::from("really"),
String::from("coming"),
],
second_half: vec![
String::from("we"),
String::from("do"),
String::from("not"),
String::from("sow"),
],
}
}
}
async fn transmit(q: Queue, tx: trpl::Sender<String>) {
// TODO: We want to send `tx` to both tasks. But currently, it is moved
// into the first task (future). What change do we need to make in order to
// solve for this issue?
let tx_fut1 = async move {
for val in q.first_half {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(250)).await;
}
};
let tx_fut2 = async move {
for val in q.second_half {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
trpl::join(tx_fut1, tx_fut2).await;
}
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let queue = Queue::new();
let tx_fut = transmit(queue, tx);
// TODO: Define an `rx_fut` async block that loops through all received values from `rx`
// and processes each one.
// TODO: Wait on the two futures (`tx_fut` and `rx_fut`) to complete.
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_messages_are_received() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let queue = Queue::new();
transmit(queue, tx).await;
let mut received = Vec::new();
while let Some(val) = rx.recv().await {
received.push(val);
}
received.sort();
let expected: Vec<String> =
vec!["coming", "do", "is", "not", "really", "sow", "we", "winter"]
.into_iter()
.map(String::from)
.collect();
assert_eq!(received, expected);
});
}
}

View File

@ -25,3 +25,4 @@
| macros | §20.5 | | macros | §20.5 |
| clippy | Appendix D | | clippy | Appendix D |
| conversions | n/a | | conversions | n/a |
| async_await | §17 |

View File

@ -1211,3 +1211,57 @@ name = "conversions5"
dir = "23_conversions" dir = "23_conversions"
hint = """ hint = """
Add `AsRef<str>` or `AsMut<u32>` as a trait bound to the functions.""" Add `AsRef<str>` or `AsMut<u32>` as a trait bound to the functions."""
# ASYNC/AWAIT
[[exercises]]
name = "async_await1"
dir = "24_async_await"
hint = """
An `async fn` returns a `Future`, not the final value. Use `.await` to run the
future to completion and get the result.
Rust doesn't ship with an async runtime in the standard library. `Tokio` is a popular
async runtime for Rust that provides an event loop and task scheduling.
The `trpl::block_on()` function runs a single future to completion on the Tokio `Runtime`.
Every time it's called, a new instance of `tokio::runtime::Runtime` is created.
See the Async Programming chapter of the Rust book:
https://doc.rust-lang.org/book/ch17-01-futures-and-syntax.html"""
[[exercises]]
name = "async_await2"
dir = "24_async_await"
hint = """
`trpl::join!` takes multiple futures and polls them concurrently. It returns a
tuple with all results once every future has completed.
Example:
```rust
let (a, b) = trpl::join!(future_a(), future_b());
```
Resource: https://doc.rust-lang.org/book/ch17-02-concurrency-with-async.html"""
[[exercises]]
name = "async_await3"
dir = "24_async_await"
hint = """
Use `trpl::spawn_task` to run a future as an independent task on the runtime.
It returns a `JoinHandle` which you can `.await` to get the task's output.
This is the async equivalent of spawning threads and calling `join` on them.
Resource: https://doc.rust-lang.org/book/ch17-02-concurrency-with-async.html"""
[[exercises]]
name = "async_await4"
dir = "24_async_await"
hint = """
The async channel is a multiple-producer channel, so we can send multiple messages
from multiple futures.
Then we can use `trpl::join!` to await multiple futures to complete. `trpl::join!`
cooperatively polls futures on the same executor thread.
Resource: https://doc.rust-lang.org/book/ch17-02-concurrency-with-async.html"""

View File

@ -0,0 +1,29 @@
// This program fetches a greeting asynchronously. Async functions return a
// `Future` that must be `.await`ed to get the result. An async runtime like
// Tokio is needed to drive futures to completion.
use std::time::Duration;
async fn fetch_greeting() -> String {
trpl::sleep(Duration::from_millis(10)).await;
String::from("Hello, async world!")
}
fn main() {
// `trpl::block_on()` runs a single future to completion on the Tokio `Runtime`.
// Every time it's called, a new instance of `tokio::runtime::Runtime` will be created.
trpl::block_on(async {
// `.await` suspends until the future completes and yields the `String`.
let greeting = fetch_greeting().await;
println!("{greeting}");
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fetch_greeting_returns_expected_message() {
trpl::block_on(async { assert_eq!(fetch_greeting().await, "Hello, async world!") });
}
}

View File

@ -0,0 +1,32 @@
// This program runs multiple async operations. The `Future` trait represents an
// asynchronous computation. Multiple futures can be polled concurrently with
// macros like `trpl::join!`.
use std::time::Duration;
async fn compute_value(id: u32) -> u32 {
trpl::sleep(Duration::from_millis(10)).await;
id * 10
}
async fn compute_all() -> Vec<u32> {
// `trpl::join!` polls all futures concurrently and returns a tuple of results.
let (a, b, c) = trpl::join!(compute_value(1), compute_value(2), compute_value(3));
vec![a, b, c]
}
fn main() {
trpl::block_on(async {
let results = compute_all().await;
println!("Computed: {results:?}");
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compute_all_returns_expected_values() {
trpl::block_on(async { assert_eq!(compute_all().await, [10, 20, 30]) });
}
}

View File

@ -0,0 +1,42 @@
// Async runtimes like Tokio can spawn independent tasks that run concurrently
// on the runtime's thread pool. Each spawned task returns a `JoinHandle` that
// can be `.await`ed to get its result — similar to `thread::spawn` and `join`.
use std::time::Duration;
async fn double(n: u32) -> u32 {
trpl::sleep(Duration::from_millis(10)).await;
n * 2
}
async fn double_all(values: &[u32]) -> Vec<u32> {
let mut handles = Vec::new();
for &value in values {
// `trpl::spawn_task` schedules the future on the runtime's executor.
handles.push(trpl::spawn_task(double(value)));
}
let mut results = Vec::new();
for handle in handles {
// Awaiting the `JoinHandle` waits for the spawned task to finish.
results.push(handle.await.unwrap());
}
results
}
fn main() {
trpl::block_on(async {
let results = double_all(&[1, 2, 3, 4, 5]).await;
println!("Results: {results:?}");
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compute_all_double_values() {
trpl::block_on(async { assert_eq!(double_all(&[1, 2, 3, 4, 5]).await, [2, 4, 6, 8, 10]) });
}
}

View File

@ -0,0 +1,97 @@
// This exercise demonstrates async message-passing concurrency using a channel.
use std::time::Duration;
struct Queue {
first_half: Vec<String>,
second_half: Vec<String>,
}
impl Queue {
fn new() -> Self {
Self {
first_half: vec![
String::from("winter"),
String::from("is"),
String::from("really"),
String::from("coming"),
],
second_half: vec![
String::from("we"),
String::from("do"),
String::from("not"),
String::from("sow"),
],
}
}
}
async fn transmit(q: Queue, tx: trpl::Sender<String>) {
// Clone the sender `tx` first.
let tx_clone = tx.clone();
let tx_fut1 = async move {
for val in q.first_half {
// Then we use the clone here
tx_clone.send(val).unwrap();
trpl::sleep(Duration::from_millis(250)).await;
}
};
let tx_fut2 = async move {
for val in q.second_half {
// And here we use the original sender `tx`
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
trpl::join(tx_fut1, tx_fut2).await;
}
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let queue = Queue::new();
let tx_fut = transmit(queue, tx);
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("Received: {value:?}");
}
};
trpl::join!(tx_fut, rx_fut); // OR `trpl::join(tx_fut, rx_fut).await`
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_messages_are_received() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let queue = Queue::new();
transmit(queue, tx).await;
let mut received = Vec::new();
while let Some(val) = rx.recv().await {
received.push(val);
}
received.sort();
let expected: Vec<String> =
vec!["coming", "do", "is", "not", "really", "sow", "we", "winter"]
.into_iter()
.map(String::from)
.collect();
assert_eq!(received, expected);
});
}
}