Understanding await in Rust Async

Challenge: Race Condition & Mutex Fix 🏎️🔒

You’re building an async counter where multiple tasks try to update a shared number at the same time. But there’s a problem: Rust doesn’t allow shared mutable access without safety measures.

Your Mission:

  1. Create a shared counter (i32) starting at 0.
  2. Spawn three async tasks that each:
    • Wait a random time (0-3 seconds).
    • Increase the counter by 1.
  3. Print the counter after all tasks finish.
  4. Fix any data race issues!

Hints:

  • Use Arc<Mutex<i32>> to safely share the counter.
  • tokio::spawn helps run multiple tasks at the same time.
  • Use tokio::time::sleep(Duration::from_secs(x)) to simulate work.
  • tokio::join! or .await to wait for all tasks.
use std::sync::{Arc, Mutex};
use tokio::time::{sleep, Duration};
use tokio::spawn;
use rand::Rng;

#[derive(Debug)]
struct Counter(i32);

#[tokio::main]
async fn main() {
    let counter = Arc::new(Mutex::new(Counter(0)));
    let mut handles = vec![];

    for _ in 0..3 {
        let count = Arc::clone(&counter);  // Clone Arc for each task
        let handle = spawn(async move {
            let wait_time = rand::thread_rng().gen_range(1..=3);
            sleep(Duration::from_secs(wait_time)).await;

            let mut ncount = count.lock().unwrap();
            ncount.0 += 1;  // Correctly increment counter
            println!("Task finished, counter is now: {}", ncount.0);
        });

        handles.push(handle);
    }

    for handle in handles {
        let _ = handle.await;
    }

    println!("Final counter value: {:?}", counter.lock().unwrap().0);
}

When you use await, it pauses the task, not the thread. Here’s the distinction:

Task vs. Thread:

  • Task: In the context of async programming, a task is a unit of work that’s scheduled by the runtime (e.g., Tokio). Each task runs asynchronously and can yield control back to the runtime.
  • Thread: A thread is the underlying system thread where tasks are executed. You can have multiple tasks running on the same thread, and these tasks can switch between each other without blocking the thread.

What Happens with await:

  • When you call await, the task pauses at that point, allowing the async runtime to run other tasks. The thread is not blocked, so other tasks can be executed on the same thread.
  • Once the future is ready (i.e., the awaited operation completes), the task resumes from where it left off.