Хранение tokio::задача::JoinHandle в хэш-карте и доступ к ней из другой задачи

#rust #async-await #rust-tokio

Вопрос:

Приветствую Вас, Растоптанные,

В настоящее время я пытаюсь перенести приложение JVM (точнее, бота discord) в rust для развлечения и обучения, используя serenity-rs, и в настоящее время я сталкиваюсь с кирпичной стеной с задачами 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(amp;"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(amp;"None");
            println!("After 35sec: {}", res)
        }
    });

    join!(handle_a, handle_b);
}
 

Который в настоящее время выдает следующую ошибку при компиляции:

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

Я завернул handle_a в an Arc , потому что хочу дождаться его «в конце основной функции», все еще имея возможность поместить ссылку на него в карту задач. Так что это, очевидно, кажется неправильным, но я не могу придумать другого способа справиться с этим. Просто вызов deref() handle_a приведет к другой ошибке:

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

Что, я думаю, имеет смысл, потому что документы для Arc штата:

Общие ссылки в Rust по умолчанию запрещают мутацию, и Arc не является исключением: обычно вы не можете получить изменяемую ссылку на что-либо внутри Дуги.

Я думаю, что «основанный на вытягивании» подход фьючерсов в Rust-это то, что действительно заставляет меня бороться здесь, потому что мне нужна ссылка на JoinHandle, чтобы дождаться ее в конце основной функции.

Вероятно, это просто совершенно неправильный подход к этой проблеме, поэтому я был бы очень благодарен за любые намеки или подсказки в правильном направлении.

Уже спасибо за потраченное время, если вы дочитали до этого места!

Правка: Исправлена опечатка, упомянутая в принятом ответе.

Ответ №1:

tokio::spawn уже выполняет будущее в фоновом режиме. Вам нужно только дождаться его, если вы хотите дождаться его результата, который вам в данном случае не нужен. Вы можете просто заменить его на b.await .

У вас также есть небольшая логическая ошибка в названии задачи — вы используете "thetask" вместо "the_task" одного запроса. Я немного очистил код, и вы можете найти рабочую версию на этой игровой площадке.


Я не знаком с дизайном ботов Discord, но если у вас есть только один «цикл событий», который работает все время и отвечает как за создание, так и за отмену задач, вам не нужно создавать для него задачу tokio::spawn , но вместо этого вы можете запустить ее непосредственно в main функции. Это означало бы, что он может владеть tasks картой исключительно, и у вас не возникнет проблем с совместным владением.

Комментарии:

1. Спасибо за указание на то, что для запуска задач tokio не нужно ждать. Похоже, я неправильно понял это, просматривая документы. Тем временем я также нашел решение, использующее асинхронные fn с Abortable AbortHandle ящиком фьючерсов и из него, но решение задачи tokio кажется намного проще. К сожалению, платформа discord основана на общей карте между вызовами обработчиков, так что мне все равно придется иметь дело с этим, но пока, похоже, она работает нормально.