diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 4f725b70..234b1c50 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -188,6 +188,10 @@ 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" }, + { name = "async2", path = "../exercises/24_async/async2.rs" }, + { name = "async2_sol", path = "../solutions/24_async/async2.rs" }, ] [package] @@ -196,6 +200,9 @@ edition = "2024" # Don't publish the exercises on crates.io! publish = false +[dependencies] +tokio = { version = "1.52.1", features = ["rt", "sync", "time"] } + [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/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 ae6e24a9..dfe72ae0 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1205,3 +1205,24 @@ 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.""" + +[[exercises]] +name = "async2" +dir = "24_async" +test = false +hint = """ +TODO""" 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); +} 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(); + } +}