haukeh
haukeh

Reputation: 182

Storing tokio::task::JoinHandle in a HashMap and accessing it from another task

Greetings Rustaceans,

I am currently trying to port a JVM app (a discord bot to be precise) to rust for fun and education, using serenity-rs, and I'm currently hitting a brick wall with tokio tasks and shared state. I'm very new to the whole rust experience, so please be gentle.

The basic idea is, that some task gets started asynchronously and after a certain time of waiting it inserts some data into a shared (concurrent) map. Another part of the program is listening for events, and if one such event occurs while the first task is still waiting, it will cancel said task, thus causing the data to not be inserted into the shared map.

I have begun with a "simplified" exampled, which looks like this and only has a dependency upon tokio.

[dependencies]
tokio = { version = "1", features = ["full"] }
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;

use tokio::join;
use tokio::sync::RwLock;
use tokio::time;

#[tokio::main]
async fn main() {
    let mut data: Arc<RwLock<HashMap<_,_>>> =
        Arc::new(RwLock::new(HashMap::new()));

    let mut tasks: Arc<RwLock<HashMap<String, _>>> =
        Arc::new(RwLock::new(HashMap::new()));

    let mut d1 = data.clone();
    let handle_a = tokio::spawn(async move {
        time::sleep(Duration::from_secs(30)).await;
        {
            let mut lock = d1.write().await;
            lock.insert("foo", "bar");
        }
    });

    let handle_a = Arc::new(handle_a);
    {
        let mut map = tasks.write().await;
        map.insert("the_task".to_string(), handle_a.clone());
    }

    let mut d2 = data.clone();
    let handle_b = tokio::spawn(async move {
        tokio::time::sleep(Duration::from_secs(10)).await;
        {
            let d = d2.read().await;
            let res = d.get("foo");
            println!("After 10sec: {}", res.unwrap_or(&"None"));
            let mut m = tasks.write().await;
            {
                let t = m.get("the_task").unwrap();
                println!("Now cancelling: {:?}", t);
                t.abort();
                let _ = m.remove("the_task");
            }
        }
        tokio::time::sleep(Duration::from_secs(25)).await;
        {
            let d = d2.read().await;
            let res = d.get("foo").unwrap_or(&"None");
            println!("After 35sec: {}", res)
        }
    });

    join!(handle_a, handle_b);
}

Which is currently giving the following error when it is compiled:

55 |     join!(handle_a, handle_b);
   |           ^^^^^^^^ `Arc<tokio::task::JoinHandle<()>>` is not a future

I have wrapped handle_a in an Arc because I want to await it "at the end of main function" while still being able to put a reference to it into the tasks map. So this obviously seems to be wrong, but I cannot think of another way how deal with this. Simply calling deref() on handle_a will give a different error:

`Future` is implemented for `&mut tokio::task::JoinHandle<()>`, but not for `&tokio::task::JoinHandle<()>

Which I guess makes sense, because the docs for Arc state:

Shared references in Rust disallow mutation by default, and Arc is no exception: you cannot generally obtain a mutable reference to something inside an Arc.

I think the "pull-based" approach of futures in Rust is what really makes me struggle here, because I need a reference to the JoinHandle to await it at the end of the main function.

Probably this is just the completely wrong approach to this problem, so I would be really thankful for any hints or nudges in the right direction.

Already thanks for your time, if you read this far!

Edit: Fixed the typo mentioned in the accepted answer.

Upvotes: 3

Views: 2426

Answers (1)

apilat
apilat

Reputation: 1510

tokio::spawn already executes the future in the background. You only need to await on it if you want to wait for its result, which you don't need in this case. You can simply replace it with b.await.

You also have a minor logic bug in the task name - you use "thetask" instead of "the_task" in one query. I've cleaned up the code a bit and you can find the working version on this playground.


I'm not familiar with the design of Discord bots but if you only have one "event-loop" which is running all of the time, responsible for both creating and cancelling tasks, you don't need to create a task for it with tokio::spawn but could instead run it directly in the main function. This would mean that it is able to own the tasks map exclusively and you would not run into problems with shared ownership.

Upvotes: 2

Related Questions