You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何Rust中tokio::join!宏无需使用await关键字?

为什么Tokio的join!宏不需要也不允许后续使用await?

在Rust和其他编程语言中,await关键字用于标记异步函数的挂起点。但和Python的gather、Swift的withTaskGroup、JavaScript的Promise.all不同,Rust Tokio库的tokio::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(())
}

这里有两个疑问:

  • 这种设计是否存在技术层面的原因?
  • 这种隐式的挂起容易让人困惑——乍看这个异步函数里没有显式的await,似乎没必要声明为async,这是不是违背了async-await模式要明确标记挂起点的初衷?

技术层面的原因

tokio::join!的设计是由它作为宏的特性和Tokio的性能目标共同决定的:

  1. 宏的内联展开逻辑
    join!本质是语法扩展宏,它会把代码直接展开成一段在当前异步上下文中轮询所有传入Future的逻辑。展开后的代码会逐个检查每个Future的状态:如果某个Future还没完成,就会让当前任务挂起,等调度器唤醒后继续轮询,直到所有Future都完成。这个过程本身就包含了await的核心逻辑,所以不需要额外调用await。

  2. 避免不必要的内存分配
    如果join!返回一个聚合Future,这个Future需要在堆上分配空间保存所有子Future的状态。而join!直接在当前async函数的栈上管理这些状态,完全避免了这次堆分配,符合Tokio追求低开销异步的设计目标。

  3. 静态类型与直接结果返回
    join!可以直接返回所有子Future结果组成的元组,类型在编译期就完全确定,不需要额外的包装类型。这让你能直接处理每个任务的结果,错误处理也更直观——比如如果某个子Future返回Result,你可以直接在元组里解构后处理,不需要额外的嵌套unwrap。

关于"隐式挂起点"的困惑

确实,刚接触Tokio的开发者容易对join!没有显式await感到困惑,但实际上它并没有违背async-await的设计初衷:

  • join!本身就是明确的挂起点标记:它的语义就是"等待这些异步任务全部完成",和await的核心作用一致,只是把多个await的逻辑合并成了更简洁的语法。
  • Rust编译器会严格检查async函数是否存在挂起点:如果你的async函数里没有任何await或者类似join!这种包含挂起逻辑的宏,编译器会直接报错,所以不用担心出现"没必要声明为async"的情况——能编译通过的async函数,必然包含挂起逻辑,只是join!把它封装起来了。

内容的提问来源于stack exchange,提问作者Louis Lac

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 20:57:00