Skip to content

Add exercise async1 - #2382

Open
senekor wants to merge 13 commits into
rust-lang:mainfrom
senekor:senekor/rvsyvlvuzyvu
Open

Add exercise async1#2382
senekor wants to merge 13 commits into
rust-lang:mainfrom
senekor:senekor/rvsyvlvuzyvu

Conversation

@senekor

@senekor senekor commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

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 can be moved to the location corresponding to the book in a later PR, to keep the diff of this one clean.

@senekor
senekor force-pushed the senekor/rvsyvlvuzyvu branch 2 times, most recently from b2f334e to 7f50737 Compare April 18, 2026 05:06
@senekor
senekor force-pushed the senekor/rvsyvlvuzyvu branch 2 times, most recently from 46533ad to abc8969 Compare April 18, 2026 21:31

@mo8it mo8it left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like

  • The requirement of adding async and .await
  • Spawning tasks, awaiting them and then checking that they are done
  • The strory about splitting tasks among different workers

I don't like

  • The usage of atomics
  • The work done in each task. Printing locks under the hood so the tasks will run mostly sequentially even if the runtime was multi-threaded
  • The story details about the boys and soccer. Some might find it a bit childish for tasks meant mainly for adults

What about letting the tasks do some calculation and return the result? Then all three results could be checked. This way, we don't need atomics or printing.

Comment thread dev/Cargo.toml Outdated
Comment thread exercises/24_async/async1.rs Outdated
Comment thread exercises/24_async/async1.rs Outdated
Comment thread exercises/24_async/async1.rs Outdated
Comment thread exercises/24_async/async1.rs Outdated
@mo8it

mo8it commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

What about using tokio::fs to open three files asynchronously, reading their content by parsing each line as a number and summing them up?

Something like that does actual work and is a valid usage for async.

@mo8it

mo8it commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

The story could be something like teachers want to calculate the mean grade for three different classes. Instead of only one teacher doing all the work or doing it sequentially, they can do it async.

@senekor

senekor commented Apr 25, 2026

Copy link
Copy Markdown
Contributor Author

Agree with most of these points. I redid the exercise to hopefully address them. I'm not sure yet about using a multi-threaded runtime and using file IO. Yes, it would be more realistic, but I'm concerned it might be more complicated and distract from the intended lesson here - building a little async/await muscle memory.

@mo8it mo8it left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the overall direction, but I think that trying to keep the exercise simple results in misleading the users. By avoiding async work, they won't get the difference to multi-threading and will be confused because the solution is slower on a single thread.

Comment thread exercises/24_async/async1.rs Outdated
Comment thread exercises/24_async/async1.rs
Comment thread exercises/24_async/async1.rs Outdated
@mo8it

mo8it commented May 9, 2026

Copy link
Copy Markdown
Contributor

Maybe somthing like this?

fn main() {
    // Async tasks need to be executed by a "runtime", which is not provided by
    // Rust's standard library. Here, we use the mainstream runtime `tokio`.
    let rt = tokio::runtime::Builder::new_current_thread()
        .build()
        .unwrap();

    let alice = rt.spawn(calculate_mean_score("a.txt"));
    let bob = rt.spawn(calculate_mean_score("b.txt"));
    let catherine = rt.spawn(calculate_mean_score("c.txt"));

    // Block the runtime on a task that awaits all three calculations.
    rt.block_on(async {
        // TODO: "await" all three tasks.
        assert_eq!(alice, 84);
        assert_eq!(bob, 89);
        assert_eq!(catherine, 76);
    });
}

// TODO: Fix the compiler errors by making the spawned function async.
fn calculate_mean_score(scores_file: &str) -> usize {
    // Read the file asynchronously
    let file = tokio::fs::read_to_string(scores_file).await.unwrap();

    // Initialize the sum and the number of scores
    let mut sum = 0;
    let mut n = 0;
    for line in file.lines() {
        // Parse every line as a score
        let score = line.parse::<usize>().unwrap();
        sum += score;
        n += 1;
    }

    sum / n
}

It is not much more complicated. Remember that this exercise is one towards the end, so the users aren't complete beginners at this point.

If we take this direction, we should add the IO tasks before.

@senekor

senekor commented May 16, 2026

Copy link
Copy Markdown
Contributor Author

One thing I'm not sure about with the file IO approach is how to show users that it's actually running concurrently? Presumably the work is still small enough that users won't actually notice the performance difference between sync and async versions. Should we run some benchmarks and display the results to users or something?

@senekor
senekor force-pushed the senekor/rvsyvlvuzyvu branch from 91fd773 to 02a877c Compare May 16, 2026 11:06
@mo8it

mo8it commented May 16, 2026

Copy link
Copy Markdown
Contributor

They don't need to feel the difference. It is enough to know that this approach is theoretically more efficient if the files were bigger.

@senekor

senekor commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

FYI I'm pretty busy now until the end of July. I made a reminder to get back to this in August :)

@senekor
senekor force-pushed the senekor/rvsyvlvuzyvu branch from c06cd14 to 0275245 Compare August 4, 2026 21:30
@senekor
senekor requested a review from mo8it August 5, 2026 04:48
Comment thread dev/Cargo.toml Outdated
Comment thread exercises/24_async/async1.rs Outdated
Comment thread rustlings-macros/info.toml Outdated
@mo8it

mo8it commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

I like the async1 exercise!

This PR introduces a feature planned for the next version: #2170
We should think about the design before committing to one. Having everything in input_files can get chaotic pretty quickly. As a user, I would expect related files to be in the same directory where main.rs of the exercise is.

Another issue: If the user edits the input file, they won't be able to complete the exercise. Currently, we offer no way to reset input files. A better approach would be to check the hash of the input files of the current exercise before running it. If they were altered, we should reset them automatically from the embedded version.

@senekor

senekor commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

I haven't yet added a check if the user changed an input file, I'll get back to that later.

Another thought: The paths are a little long right now. What do you think about executing exercises with their exercise directory as the CWD? That would shorten the path for reading input files. Also, it might be easier for text editors to recognize the input files as relative paths to the currently openend file.

Comment thread src/dev/check.rs Outdated
@mo8it

mo8it commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Looks good so far!
Definitely set CWD for running the binary. It should be enough to add it to the command here:

run_cmd(Command::new(&bin_path), bin_name, output)

After thinking about it again, maybe checking if the file is not changed is less efficient than just dumping it every time before executing the binary (not before check and build). This way, we don't have to open the file, read it and calculate its hash.

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.
- Remove confusing use of atomics. Use return values of async tasks
  instead, to ensure all tasks are awaited.
- Remove use of `println!()`, which uses a global lock and cannot be
  executed in parallel.
@senekor
senekor force-pushed the senekor/rvsyvlvuzyvu branch from d343ae3 to c114fb3 Compare August 10, 2026 20:28
@senekor
senekor requested a review from mo8it August 10, 2026 20:32
@mo8it

mo8it commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Force pushing forces me to rereview everything 😅 It's easier to push new commits after a review. Don't worry too much about the history. I will review later.

@senekor

senekor commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Sorry, there was a trivial merge conflict. The existing commits are unchanged. I guess you would've preferred I merge main into my branch?

@tshepang

Copy link
Copy Markdown
Member

Force pushing forces me to rereview everything 😅 It's easier to push new commits after a review.

triagebot can help: https://forge.rust-lang.org/triagebot/range-diff.html

@Chuhan-Mateo

Copy link
Copy Markdown

I wish this could be merge soon! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants