tokio::try_join!当其中一项任务 return 出错时,return 不是 Err 变体吗?

tokio::try_join! doesn't return the Err variant when one of the tasks returns Err?

我无法理解 tokio::try_run! 和任务 运行ning 在 tokio::spawn returning 和 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! 文档说

The try_join! macro returns when all branches return with Ok or when the first branch returns with Err.

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

时取消 h2

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

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

首先你必须了解期货是如何运作的。 rust async book 是一个很好的起点。

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

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

async fn foo(){
    // do something
}

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

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

生成任务

你可以用spawn()的方法把轮询未来的责任交给运行时间。当你这样做时,你不再需要(也不能)在 future 上调用 .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!() 驱动连接手柄,而在另一种情况下它驱动期货本身。

你的另一个问题

Do I need to remove tokio::spawn and forfeit parallel execution between h1 and h2 if I want h2 to be canceled when h1 returns an error?

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

在评论中回答你的问题:

Now, this raises a second question (and I think the source of my confusion): even when using tokio::spawn, select! does cancel h2 (i.e. no need for abort(), and h2 doesn't print the h2 didn't get canceled line). This seems to be what's weird to me: while select and join seem kind of similar, their behavior is the opposite.

这里的问题是您的应用程序到达了 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