From abc8969dd02a06f479774af894381997199d1963 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Fri, 17 Apr 2026 15:14:01 +0200 Subject: [PATCH 1/2] Add exercise async1 The goal here was to get the first bit of "muscle memory" for using the async and await keywords. The little story should make it more intuitive for users why asynchronous programming is needed in the first place. This exercise will be moved to the location corresponding to the book in a later commit, to keep the diff of this one clean. --- dev/Cargo.toml | 5 ++++ exercises/24_async/README.md | 13 +++++++++ exercises/24_async/async1.rs | 55 ++++++++++++++++++++++++++++++++++++ rustlings-macros/info.toml | 14 +++++++++ solutions/24_async/async1.rs | 53 ++++++++++++++++++++++++++++++++++ 5 files changed, 140 insertions(+) create mode 100644 exercises/24_async/README.md create mode 100644 exercises/24_async/async1.rs create mode 100644 solutions/24_async/async1.rs diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 4f725b70..3583e59d 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -188,6 +188,8 @@ bin = [ { name = "try_from_into_sol", path = "../solutions/23_conversions/try_from_into.rs" }, { name = "as_ref_mut", path = "../exercises/23_conversions/as_ref_mut.rs" }, { name = "as_ref_mut_sol", path = "../solutions/23_conversions/as_ref_mut.rs" }, + { name = "async1", path = "../exercises/24_async/async1.rs" }, + { name = "async1_sol", path = "../solutions/24_async/async1.rs" }, ] [package] @@ -196,6 +198,9 @@ edition = "2024" # Don't publish the exercises on crates.io! publish = false +[dependencies] +tokio = { version = "1.52.1", features = ["rt"] } + [profile.release] panic = "abort" diff --git a/exercises/24_async/README.md b/exercises/24_async/README.md new file mode 100644 index 00000000..7928650e --- /dev/null +++ b/exercises/24_async/README.md @@ -0,0 +1,13 @@ +# Async + +Asynchronous programming is a model where tasks are delegated to a runtime that executes them concurrently. +It is particularly efficient for applications where many independent IO-operations are performed, e.g. web servers. + +Rust provides the necessary primitives to do asynchronous programming in the language. +However, Rust's standard library does not include a runtime. +For these exercises, we will use the popular runtime called `tokio`. + +## Further information + +- [Fundamentals of Asynchronous Programming](https://doc.rust-lang.org/book/ch17-00-async-await.html) +- [Tokio documentation](https://docs.rs/tokio/latest/tokio/) diff --git a/exercises/24_async/async1.rs b/exercises/24_async/async1.rs new file mode 100644 index 00000000..5810ced3 --- /dev/null +++ b/exercises/24_async/async1.rs @@ -0,0 +1,55 @@ +// Tim has to complete a few chores today, before he's allowed to play soccer +// with his friends. His friends decide to help him. Working together, they +// finish the chores earlier and have more time left to play soccer. +// +// Let's simulate this using asynchronous programming. Each boy is represented +// as an asynchronous task, which can be executed concurrently (they can be +// working at the same time). + +use std::sync::atomic::{AtomicU8, Ordering}; + +// Used by "mom" to check that all chores are done before Tim plays soccer :-) +static CHORES_DONE: AtomicU8 = AtomicU8::new(0); + +fn main() { + // Async tasks need to be executed by a "runtime", which is not provided by + // Rust's standard library. We use the popular "tokio" runtime here. + let rt = tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(); + + // TODO: Fix the compiler errors by making the spawned function async. + let task_tim = rt.spawn(tim()); + let task_carl = rt.spawn(carl()); + let task_nick = rt.spawn(nick()); + + // Block the runtime on a task that waits for all boys to finish the chores. + // TODO: "await" all three tasks to fix the compiler errors. + rt.block_on(async { + task_tim; + task_carl; + task_nick; + }); + + assert_eq!( + CHORES_DONE.load(Ordering::SeqCst), + 3, + "Did you (a)wait for all the boys to finish the chores?" + ); + println!("Ready to play soccer!"); +} + +fn tim() { + println!("Cleaning my room..."); + CHORES_DONE.fetch_add(1, Ordering::SeqCst); +} + +fn carl() { + println!("Washing the dishes..."); + CHORES_DONE.fetch_add(1, Ordering::SeqCst); +} + +fn nick() { + println!("Mowing the lawn..."); + CHORES_DONE.fetch_add(1, Ordering::SeqCst); +} diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 353d405c..8266ea99 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1199,3 +1199,17 @@ name = "as_ref_mut" dir = "23_conversions" hint = """ Add `AsRef` or `AsMut` as a trait bound to the functions.""" + +# ASYNC + +[[exercises]] +name = "async1" +dir = "24_async" +test = false +hint = """ +Asynchronous runtimes like tokio can only spawn tasks that are defined as async +functions, not regular ones. Add the "async" keyword before the "fn" keyword of +the functions "tim", "carl" and "nick". + +An async task can wait for another one to complete by "awaiting" it. Add +".await" after the three "task_name" variables in the "block_on" call.""" diff --git a/solutions/24_async/async1.rs b/solutions/24_async/async1.rs new file mode 100644 index 00000000..a897067a --- /dev/null +++ b/solutions/24_async/async1.rs @@ -0,0 +1,53 @@ +// Tim has to complete a few chores today, before he's allowed to play soccer +// with his friends. His friends decide to help him. Working together, they +// finish the chores earlier and have more time left to play soccer. +// +// Let's simulate this using asynchronous programming. Each boy is represented +// as an asynchronous task, which can be executed concurrently (they can be +// working at the same time). + +use std::sync::atomic::{AtomicU8, Ordering}; + +// Used by "mom" to check that all chores are done before Tim plays soccer :-) +static CHORES_DONE: AtomicU8 = AtomicU8::new(0); + +fn main() { + // Async tasks need to be executed by a "runtime", which is not provided by + // Rust's standard library. We use the popular "tokio" runtime here. + let rt = tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(); + + let task_tim = rt.spawn(tim()); + let task_carl = rt.spawn(carl()); + let task_nick = rt.spawn(nick()); + + // Block the runtime on a task that waits for all boys to finish the chores. + rt.block_on(async { + task_tim.await.unwrap(); + task_carl.await.unwrap(); + task_nick.await.unwrap(); + }); + + assert_eq!( + CHORES_DONE.load(Ordering::SeqCst), + 3, + "Did you (a)wait for all the boys to finish the chores?" + ); + println!("Ready to play soccer!"); +} + +async fn tim() { + println!("Cleaning my room..."); + CHORES_DONE.fetch_add(1, Ordering::SeqCst); +} + +async fn carl() { + println!("Washing the dishes..."); + CHORES_DONE.fetch_add(1, Ordering::SeqCst); +} + +async fn nick() { + println!("Mowing the lawn..."); + CHORES_DONE.fetch_add(1, Ordering::SeqCst); +} From 09cd4bd50894ae999fc1080f51ac4c264f80fed6 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sat, 18 Apr 2026 23:32:07 +0200 Subject: [PATCH 2/2] Add exercise async2 --- dev/Cargo.toml | 4 +- exercises/24_async/async2.rs | 95 ++++++++++++++++++++++++++++++++++++ rustlings-macros/info.toml | 7 +++ solutions/24_async/async2.rs | 95 ++++++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 exercises/24_async/async2.rs create mode 100644 solutions/24_async/async2.rs diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 3583e59d..234b1c50 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -190,6 +190,8 @@ bin = [ { name = "as_ref_mut_sol", path = "../solutions/23_conversions/as_ref_mut.rs" }, { name = "async1", path = "../exercises/24_async/async1.rs" }, { name = "async1_sol", path = "../solutions/24_async/async1.rs" }, + { name = "async2", path = "../exercises/24_async/async2.rs" }, + { name = "async2_sol", path = "../solutions/24_async/async2.rs" }, ] [package] @@ -199,7 +201,7 @@ edition = "2024" publish = false [dependencies] -tokio = { version = "1.52.1", features = ["rt"] } +tokio = { version = "1.52.1", features = ["rt", "sync", "time"] } [profile.release] panic = "abort" diff --git a/exercises/24_async/async2.rs b/exercises/24_async/async2.rs new file mode 100644 index 00000000..bdaf30c4 --- /dev/null +++ b/exercises/24_async/async2.rs @@ -0,0 +1,95 @@ +// Two people are talking on the phone. One of them is telling a story. The +// other one is interjecting with little acknowledgments, to show their interest +// in the story. +// +// However, there is a problem. The phone connection is synchronous, so all +// the acknowledgments from the listener arrive only at the very end of the +// conversation! What the speaker and listener say should be interleaved. +// +// Let's use asynchronous programming to make the conversation more natural! + +use std::time::Duration; + +use tokio::sync::mpsc; + +fn main() { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .unwrap(); + let _guard = rt.enter(); + + let time_scale = Duration::from_millis(1); + + let (speaker_phone, listener_phone, mut wire_tap) = start_wire_tapped_phone_call(); + + let speaker = async move { + for msg in SPEAKER_MESSAGES { + speaker_phone.say(msg).await; + // wait for listener to interject + wait_silently(time_scale * 2).await; + } + }; + let listener = async move { + // give speaker a head-start + wait_silently(time_scale * 1).await; + for msg in LISTENER_MESSAGES { + listener_phone.say(msg).await; + // wait for speaker to continue story + wait_silently(time_scale * 2).await; + } + }; + tokio::spawn(speaker); + tokio::spawn(listener); + + let messages: Vec<_> = std::iter::from_fn(|| rt.block_on(wire_tap.recv())).collect(); + for message in &messages { + println!("{message}"); + } + let expected = SPEAKER_MESSAGES + .iter() + .zip(LISTENER_MESSAGES) + .flat_map(|(&a, &b)| [a, b]); + for (expected, message) in expected.zip(messages) { + assert_eq!(message, expected, "") + } +} + +async fn wait_silently(duration: Duration) { + // TODO: The sleep function from the standard library blocks the current + // thread, preventing other async tasks from progressing. The tokio + // library, which provides our async runtime, can help: + // https://docs.rs/tokio/latest/tokio/time/fn.sleep.html + std::thread::sleep(duration); +} + +const SPEAKER_MESSAGES: &[&str] = &[ + "> So I was walking in the park...", + "> where I met Susan by coincidence...", + "> and she was wearing a purple hat!", +]; +const LISTENER_MESSAGES: &[&str] = &[ + " I see. <", + " Oh, really? <", + " No way! <", +]; + +/// This phone is wire-tapped for testing purposes. +#[derive(Clone)] +struct Phone { + sender: mpsc::Sender<&'static str>, +} + +// Create a wire-tapped phone call. +fn start_wire_tapped_phone_call() -> (Phone, Phone, mpsc::Receiver<&'static str>) { + let (sender, wire_tap) = mpsc::channel(6); + let phone = Phone { sender }; + (phone.clone(), phone, wire_tap) +} + +impl Phone { + /// Say something on the phone. + async fn say(&self, thing: &'static str) { + self.sender.send(thing).await.unwrap(); + } +} diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 8266ea99..84b6e5aa 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1213,3 +1213,10 @@ the functions "tim", "carl" and "nick". An async task can wait for another one to complete by "awaiting" it. Add ".await" after the three "task_name" variables in the "block_on" call.""" + +[[exercises]] +name = "async2" +dir = "24_async" +test = false +hint = """ +TODO""" diff --git a/solutions/24_async/async2.rs b/solutions/24_async/async2.rs new file mode 100644 index 00000000..c7ef6b8a --- /dev/null +++ b/solutions/24_async/async2.rs @@ -0,0 +1,95 @@ +// Two people are talking on the phone. One of them is telling a story. The +// other one is interjecting with little acknowledgments, to show their interest +// in the story. +// +// However, there is a problem. The phone connection is synchronous, so all +// the acknowledgments from the listener arrive only at the very end of the +// conversation! What the speaker and listener say should be interleaved. +// +// Let's use asynchronous programming to make the conversation more natural! + +use std::time::Duration; + +use tokio::sync::mpsc; + +fn main() { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .unwrap(); + let _guard = rt.enter(); + + let time_scale = Duration::from_millis(1); + + let (speaker_phone, listener_phone, mut wire_tap) = start_wire_tapped_phone_call(); + + let speaker = async move { + for msg in SPEAKER_MESSAGES { + speaker_phone.say(msg).await; + // wait for listener to interject + wait_silently(time_scale * 2).await; + } + }; + let listener = async move { + // give speaker a head-start + wait_silently(time_scale * 1).await; + for msg in LISTENER_MESSAGES { + listener_phone.say(msg).await; + // wait for speaker to continue story + wait_silently(time_scale * 2).await; + } + }; + tokio::spawn(speaker); + tokio::spawn(listener); + + let messages: Vec<_> = std::iter::from_fn(|| rt.block_on(wire_tap.recv())).collect(); + for message in &messages { + println!("{message}"); + } + let expected = SPEAKER_MESSAGES + .iter() + .zip(LISTENER_MESSAGES) + .flat_map(|(&a, &b)| [a, b]); + for (expected, message) in expected.zip(messages) { + assert_eq!(message, expected, "") + } +} + +async fn wait_silently(duration: Duration) { + // TODO: The sleep function from the standard library blocks the current + // thread, preventing other async tasks from progressing. The tokio + // library, which provides our async runtime, can help: + // https://docs.rs/tokio/latest/tokio/time/fn.sleep.html + tokio::time::sleep(duration).await; +} + +const SPEAKER_MESSAGES: &[&str] = &[ + "> So I was walking in the park...", + "> where I met Susan by coincidence...", + "> and she was wearing a purple hat!", +]; +const LISTENER_MESSAGES: &[&str] = &[ + " I see. <", + " Oh, really? <", + " No way! <", +]; + +/// This phone is wire-tapped for testing purposes. +#[derive(Clone)] +struct Phone { + sender: mpsc::Sender<&'static str>, +} + +// Create a wire-tapped phone call. +fn start_wire_tapped_phone_call() -> (Phone, Phone, mpsc::Receiver<&'static str>) { + let (sender, wire_tap) = mpsc::channel(6); + let phone = Phone { sender }; + (phone.clone(), phone, wire_tap) +} + +impl Phone { + /// Say something on the phone. + async fn say(&self, thing: &'static str) { + self.sender.send(thing).await.unwrap(); + } +}