【问题标题】:tokio::try_join! doesn't return the Err variant when one of the tasks returns Err?东京::try_join!当其中一项任务返回 Err 时不返回 Err 变体?
【发布时间】:2021-09-02 23:10:18
【问题描述】:

我无法理解tokio::try_run! 和在tokio::spawn 中运行的任务之间的交互,返回一个Err。 当我运行以下示例时:

use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let h1 = tokio::spawn(async {
        sleep(Duration::from_millis(100)).await;
        // 1/0; commented for now
        let v: Result<i32, ()> = Err(());
        v
    });

    let h2 = tokio::spawn(async {
        sleep(Duration::from_millis(500)).await;
        println!("h2 didn't get canceled");
        let v: Result<i32, ()> = Ok(2);
        v
    });

    match tokio::try_join!(h1, h2) {
        Ok((first, second)) => {
            println!("try_join was successful, got {:?} and {:?}", first, second);
        }
        Err(err) => {
            println!("try_join had an error: {:?}", err);
        }
    }
}

打印出来

h2 didn't get canceled
try_join was successful, got Err(()) and Ok(2)

但是,我希望它会打印出类似于我在 h1 中取消注释除以零时发生的情况:

thread 'tokio-runtime-worker' panicked at 'attempt to divide by zero', src/bin/select-test.rs:7:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
try_join had an error: JoinError::Panic(...)

try_join! 文档说

try_join!当所有分支返回 Ok 或第一个分支返回 Err 时宏返回。

但是,在我发布的示例中,h1 确实返回 Errtry_join! 执行 Ok 变体。此外,h2 不会被取消,它会运行到完成即使 h1 在数百毫秒之前已经失败了。我不明白这是否与文档相矛盾。此外,我似乎无法实现我想要做的事情,即当 h1 返回 Err 时取消 h2。

经过反复试验,我发现当我从 h1 和 h2 中删除 tokio::spawn 时,try_join! 确实以我预期的方式执行并调用了 Err 变体。不过,我不明白为什么这会有所作为。

谁能提供更多信息来解释为什么会出现这种行为?如果我希望在 h1 返回错误时取消 h2,是否需要删除 tokio::spawn 并放弃 h1 和 h2 之间的并行执行?

【问题讨论】:

    标签: rust async-await rust-tokio


    【解决方案1】:

    首先,您必须了解期货的运作方式。 rust async book 是一个很好的起点。

    与自行取得进展的线程不同,必须轮询未来。如果它没有被轮询,它不会做任何事情。所以有两种方法可以做到这一点:

    作为另一个异步函数的一部分:

    async fn foo(){
        // do something
    }
    
    async fn bar(){
        foo().await; // here foo() is being polled
    }
    
    

    这种方法的问题在于,有人需要推动未来。这里bar() 正在驾驶foo(),但它不会做任何事情,除非有人开车 bar() - (即调用它的poll()方法)

    生成任务

    您可以使用spawn() 方法将轮询未来的责任移交给运行时。当您这样做时,您不再需要(也不能)在未来致电.await。现在任务计划程序将为您完成这项工作。

    回到问题

    那么为什么它在你的情况下不起作用?

    let h1 = tokio::spawn(async {...});
    let h2 = tokio::spawn(async {...});
    

    它不起作用,因为您正在生成任务。把它想象成你正在启动两个彼此独立工作的线程(尽管你不是)。您不再负责轮询期货 - 运行时将为您执行此操作。无论是否轮询它们的连接句柄,这两个任务都将运行完成。

    我猜你的困惑来自连接句柄 h1h2 - 是的 - 你可以 .await 这些,但他们只能告诉你任务是否完成 - 他们会不会 驱动实际任务 - tokio 调度程序将。你可以把它们想象成thread 的连接句柄——不管你是不是 .join() 线程——它仍然会在后台运行。这就是为什么h2 仍然运行到完成 - 因为任务仍在由调度程序轮询 - try_join!() 宏没有驱动任务。

    当您不生成它们时,try_join!() 正在驱动任务。它正在在实际未来调用.poll(),因此当task-1 完成时,它停止在task-2 上调用.poll(),从而有效地取消它。

    TLDR:当生成时,try_join!() 正在驱动连接句柄,而在另一种情况下,它正在驱动期货本身。

    您的其他问题

    如果我希望在 h1 返回错误时取消 h2,是否需要删除 tokio::spawn 并放弃 h1h2 之间的并行执行?

    否 - 您可以使用 JoinHandle::abort() 手动取消第二个任务

    在 cmets 中回答您的问题:

    现在,这提出了第二个问题(我认为这是我困惑的根源):即使使用 tokio::spawn,选择!确实取消了 h2(即不需要 abort(),并且 h2 不打印 h2 没有被取消的行)。这对我来说似乎很奇怪:虽然 select 和 join 看起来有点相似,但它们的行为恰恰相反。

    这里的问题是您的应用程序到达main() 的末尾,因此您的整个运行时都停止了,一切都被取消了。如果您在末尾添加简短的sleep(),您将看到您的消息:

    tokio::select! {
        _ = h1 => println!("H1"),
        _ = h2 => println!("H2"),
    }
    
    sleep(Duration::from_secs(2)).await;
    

    结果:

    H1
    h2 didn't get canceled
    
    Process finished with exit code 0
    

    【讨论】:

    • 哦,哇,我不知道JoinHandle::abort()!我回到我的示例,发现它并不直接适用:try_join! 仅在所有子任务结束后才返回。但是,如果我将try_join! 替换为select!,那么我可以使用abort
    • 现在,这引发了第二个问题(我认为这是我困惑的根源):如果我将 tokio::try_join 替换为 select!,即使是 tokio::spawn,h2 确实 i> 通过选择取消(即不需要abort(),并且 h2 不打印“h2 没有被取消”行)。这对我来说似乎很奇怪:虽然 select 和 join 看起来有点相似,但在这种情况下它们的行为却大不相同。
    • 非常感谢如此!我没有意识到我被提前终止程序所愚弄。这非常有用,完全消除了我对这些原始工作形成的一堆误解!
    猜你喜欢
    • 1970-01-01
    • 2013-01-30
    • 2014-12-09
    • 1970-01-01
    • 1970-01-01
    • 2018-05-12
    相关资源
    最近更新 更多