BudiBadu Logo
Samplebadu

Rust by Example: Concurrent File Download

Rust 1.75+

Execute multiple async operations in parallel. This practical example demonstrates how to use join_all to perform concurrent downloads, significantly reducing total wait time compared to sequential execution.

Code

use futures::future::join_all;
use tokio::time::{sleep, Duration};

async fn download_file(url: &str) -> String {
    println!("Downloading {}...", url);
    // Simulate network latency
    sleep(Duration::from_millis(500)).await;
    format!("Content of {}", url)
}

#[tokio::main]
async fn main() {
    let urls = vec![
        "https://example.com/file1",
        "https://example.com/file2",
        "https://example.com/file3",
    ];

    println!("Starting downloads...");
    let start = std::time::Instant::now();

    // Create a vector of Futures
    // Note: This does NOT start the tasks yet if we just map them.
    // However, because we are calling an async fn, the futures are created.
    // To run them concurrently, we need to drive them all at once.
    let futures: Vec<_> = urls.into_iter()
                              .map(|url| download_file(url))
                              .collect();

    // join_all takes a collection of futures and returns a single future
    // that completes when ALL of them are complete.
    let results = join_all(futures).await;

    let duration = start.elapsed();
    println!("Downloaded {} files in {:?}", results.len(), duration);
    
    for res in results {
        println!(" - {}", res);
    }
}

Explanation

One of the main benefits of async/await is the ability to perform multiple I/O operations concurrently. If you were to download 3 files synchronously, it would take 3 times the latency of one file. With async, you can start all 3 downloads at once and wait for them all to finish, taking only as much time as the slowest download.

The futures::future::join_all function is a common utility for this. It takes a list of futures and returns a new future that resolves to a list of results. This is similar to Promise.all in JavaScript.

It is important to note that join_all runs these futures concurrently on the same task (interleaved execution). If you want true parallelism (running on different CPU cores), you should use tokio::spawn for each download to create separate tasks, and then await their handles.

Code Breakdown

26
urls.into_iter().map(...). This creates an iterator of Futures. Crucially, creating a future is cheap and does not start the work until polled.
32
join_all(futures). This combinator aggregates the futures. It polls the first one, then the second, etc. When one yields, it moves to the next.
32
.await. This drives the combined future. It will not complete until every sub-future has completed.
34
duration. You will notice the total time is roughly 500ms (the time of one download), not 1500ms, proving that the operations ran concurrently.