为什么 `tokio::join!` 宏在 Rust 中不需要 `await` 关键字?

huangapple go评论71阅读模式
英文:

Why doesn't the `tokio::join!` macro require the `await` keyword in Rust?

问题

在Rust(以及其他编程语言)中,await关键字用于指示异步函数体中的暂停点。然而,与其他编程语言(如Python的gather,Swift的withTaskGroup,JavaScript的Promise.all等)相反,Rust不需要也不允许在Tokyo::join!宏后面使用await关键字。

例如,在这个async函数中,join!后面没有涉及await

use tokio::{io::Result, time::{sleep, Duration}, join};

async fn work() -> String {
    sleep(Duration::from_secs(2)).await;
    String::from("Work done")
}

#[tokio::main]
async fn main() -> Result<()> {
    println!("Awaiting…");
    let (o1, o2) = join!(
        work(),
        work(),
    );  // 没有 `.await`
    println!("{}, {}", o1, o2);
    Ok(())
}

join!宏为什么不返回某种聚合的Future或可以进行await的连接句柄,是否存在技术原因?

这种隐式的await令人困惑,因为这个异步函数乍一看似乎没有任何暂停点(因此不需要标记为async),我认为这可能违反了async-await模式的初衷,该模式旨在使暂停点明确。

英文:

In Rust (and other programming languages as well), the await keyword is used to indicate a suspension point in the body of an asynchronous function. However, I noted that contrary to other programming languages (Python gather, Swift withTaskGroup, JavaScript Promise.all, etc.), Rust does not require and will refuse the presence of an await after the Tokyo::join! macro.

For instance in this async function there is no await involved after join!:

use tokio::{io::Result, time::{sleep, Duration}, join};

async fn work() -&gt; String {
    sleep(Duration::from_secs(2)).await;
    String::from(&quot;Work done&quot;)
}

#[tokio::main]
async fn main() -&gt; Result&lt;()&gt; {
    println!(&quot;Awaiting&quot;);
    let (o1, o2) = join!(
        work(),
        work(),
    );  // No `.await`
    println!(&quot;{}, {}&quot;, o1, o2);
    Ok(())
}

Is there a technical reason why the join! macro does not return some sort of aggregated Future or join handle that could be awaited on?

This implicit await is confusing since this asynchronous function appears to not have any suspension point at first sight (thus not requiring to be async) and I think that this may defeat the purpose of the async-await pattern that seeks to make suspension points explicit.

答案1

得分: 3

以下是翻译好的部分:

如果您展开宏(可以在Rust Playground的“工具”菜单中完成此操作),您将看到类似于以下代码的代码:

let (o1, o2) = {
    // [一些导入]
    let mut futures = (maybe_done(work()), maybe_done(work()));
    // [一些更多的设置]
    poll_fn(move |cx| {
        // [内部]
    }).await
};

末尾的await是扩展中唯一的await。因此,从技术上讲,宏本可以编写成不包含await。然而,让我们看看历史。

join宏是在commit 7079bcd60975f592e08fcd575991f6ae2a409a1fPR #2158)中引入的,那里没有太多的讨论,但在futures库中有一个前身,是在commit d67e2936c21a4d663814e38b06ce38d85bb02e9bPR #1051)中引入的。

关于那个最初的实现,有一点需要注意,与目前在Tokio中的实现不同,它确实在async上下文中大量依赖(poll!pending!宏依赖于它):

...
loop {
    let mut all_done = true;
    $(
        if let ::core::task::Poll::Pending = poll!($fut.reborrow()) {
            all_done = false;
        }
    )*
...

此外,在PR中进行了一些讨论:

MajorBreakfast 在2018年6月28日评论道

  • 应该明确区分用于async函数的宏和其他宏之间可能会有一些明显的区别。内部使用await的宏应该以某种方式标记。或者它们都可以使用async块,并要求在它们周围加上await,例如 await!(join![a, b])。这看起来很糟糕,因为await!目前是一个宏,但在将来不会是。虽然这样的模型更容易理解,而且可以在任何地方使用,而不仅仅在async函数中。

cramertj 在2018年6月28日评论道

@MajorBreakfast select!需要在内部执行await!(而不是提供未来作为结果),以允许编译器更聪明地处理控制流(例如,允许return正常工作,启用初始化检查以理解发生break的不同条件等)。由于select!不返回未来,我认为join!返回一个未来会令人困惑——这两者似乎在某种程度上自然地组合在一起,并且似乎令人惊讶的是它们在这方面的工作方式不同。

因此,select!受益于不引入新的async块,而是将自身合并到调用者的async块中,而join!被设计为具有与select!相同的特性,尽管它本身并不受益。

英文:

If you expand the macro (this can be done on the Rust Playground with the Tools menu), you'll see code that looks like this:

let (o1, o2) = {
    // [some imports]
    let mut futures = (maybe_done(work()), maybe_done(work()));
    // [some more setup]
    poll_fn(move |cx| {
        // [innards]
    }).await
};

That await at the end is the only await in the expansion. So, there is no technical reason why the macro couldn't have been written to not contain the await. However, let's look at the history.

The join macro was introduced in commit 7079bcd60975f592e08fcd575991f6ae2a409a1f (PR #2158) and there wasn't much discussion there, but it had a predecessor in the futures library, introduced in commit d67e2936c21a4d663814e38b06ce38d85bb02e9b (PR #1051) there.

One thing to note about that original implementation is that, unlike the one that is current in Tokio, it is heavily based on being inside an async context (the poll! and pending! macros depend on it):

...
loop {
    let mut all_done = true;
    $(
        if let ::core::task::Poll::Pending = poll!($fut.reborrow()) {
            all_done = false;
        }
    )*
...

Additionally, there was some discussion in the PR:

> MajorBreakfast commented on Jun 28, 2018
>
> * There probably should be some clear distinction between macros intended for use in async functions and other macros. Macros that internally use await should be marked somehow. Alternatively they could both use an async block and require an await around them await!(join![a, b]). This just looks so bad because await! is currently a macro, but it won't be in the future. The mental model is easier though and you could use it everywhere not just in async functions.

> cramertj commented on Jun 28, 2018
>
> @MajorBreakfast select! needs to do the await! internally (rather than providing a future as a result) in order to allow the compiler to be smarter about control-flow (e.g. allowing return to work normally, enabling the initialized-ness checks to understand the different conditions under which a break can occur, etc.). Since select! doessn't return a future, I thought it would be confusing for join! to return a future-- the two seem like somewhat natural doubles, and it seems surprising that they would work differently in that respect.

So, select! benefits from not introducing a new async block but incorporating itself into the caller's async block, and join! was designed to have the same character as select!, even though it itself does not benefit.

huangapple
  • 本文由 发表于 2023年5月25日 17:19:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/76330690.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定