Search code examples
for-looprustrust-tokio

How do I await all spawned JoinHandles in a for loop


I have the following code:

#[tokio::main]
async fn main() {
    let db: Db = Arc::new(Mutex::new(HashMap::new()));
    let block_res = get_block_addresses().await;

    match block_res {
        Ok(v) => {
            println!("Block downloaded, grabbing contracts");
            println!("Txs: {}", v.result.transactions.len());
            for obj in v.result.transactions {
                let db1 = db.clone();
                let db2 = db.clone();

                let to = obj.to.clone();
                let from = obj.from.clone();

                tokio::spawn(async move {
                    let resp = check_if_contract(to, db1).await;
                });

                tokio::spawn(async move {
                    let resp = check_if_contract(from, db2).await;
                });
            }



        }
        Err(e) => {
            println!("error parsing header: {:?}", e);
        }
    }
}

As you can see it never awaits the result of the spawns. How can I properly await these JoinHandles after the for loop has done it's thing? I wanted to avoid using an array of tasks and iterating over them. Since I had seen in Jon Gjengset's video that there exists something like a NotifyHandle, that stores tasks by id. But I have no clue how to use it, or if it even makes sense in this context. I'm new to rust and async programming so I hope my question made some sense.


Solution

  • The most efficient way at this point in time for a dynamic amount of tasks is to utilize FuturesUnordered, which represents a collection of Futures where one can wait on the next Future to resolve.

    In your case it would be something along:

    let tasks = FuturesUnordered::new();
    
    for obj in v.result.transactions {
        let db1 = db.clone();
        let db2 = db.clone();
    
        let to = obj.to.clone();
        let from = obj.from.clone();
    
        tasks.push(tokio::spawn(async move {
            check_if_contract(to, db1).await
        }));
    
        tasks.push(tokio::spawn(async move {
            check_if_contract(from, db2).await
        }));
    }
    
    while let Some(finished_task) = tasks.next().await {
        match finished_task {
            Err(e) => { /* e is a JoinError - the task has panicked */},
            Ok(result) => { /* result is the result of check_if_contract */ }
        }
    }
    

    If you need to identify the transaction in the final loop, return something which identifies the task from the functions you spawn - then it will be part of result.