东京::尝试加入!当任务之一返回 Err 时不返回 Err 变体?

jgp*_*iva 4 rust async-await rust-tokio

我无法理解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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

它打印

h2 didn't get canceled
try_join was successful, got Err(()) and Ok(2)
Run Code Online (Sandbox Code Playgroud)

但是,我希望它打印出类似于我在 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(...)
Run Code Online (Sandbox Code Playgroud)

文档try_join!

尝试加入!当所有分支返回 Ok 或第一个分支返回 Err 时,宏返回。

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

经过更多的试验和错误,我发现当我tokio::spawn从 h1 和 h2 中删除 时,try_join!确实以我预期的方式执行并调用Err变体。尽管如此,我还是不明白为什么这会产生影响。

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

Sve*_*rev 5

首先你必须了解期货是如何运作的。Rust异步书是一个很好的起点。

与自行取得进展的线程不同,必须对 future 进行轮询。如果不进行轮询,它将不会执行任何操作。所以有两种方法可以做到这一点:

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

async fn foo(){
    // do something
}

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

Run Code Online (Sandbox Code Playgroud)

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

生成任务

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

回到问题

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

let h1 = tokio::spawn(async {...});
let h2 = tokio::spawn(async {...});
Run Code Online (Sandbox Code Playgroud)

它不起作用,因为您正在生成任务。可以将其想象为您正在启动两个彼此独立工作的线程(尽管实际上不是)。您不再负责轮询 future - 运行时将为您做这件事。无论它们的连接句柄是否被轮询,这两个任务都将运行完成。

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

当你不生成它们时,它们try_join!() 就会驱动任务。它正在调用.poll()实际的 future,因此当任务 1 完成时,它会停止调用.poll()任务 2,从而有效地取消它。

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

你的另一个问题

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

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

在评论中回答您的问题:

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

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

tokio::select! {
    _ = h1 => println!("H1"),
    _ = h2 => println!("H2"),
}

sleep(Duration::from_secs(2)).await;
Run Code Online (Sandbox Code Playgroud)

结果是:

H1
h2 didn't get canceled

Process finished with exit code 0
Run Code Online (Sandbox Code Playgroud)