什么时候应该使用 tokio::join!() 而不是 tokio::spawn()?

Fél*_*ger 27 rust rust-tokio

假设我想用 Tokio 同时下载两个网页......

我可以用以下方法来实现tokio::spawn()

async fn v1() {
    let t1 = tokio::spawn(reqwest::get("https://example.com"));
    let t2 = tokio::spawn(reqwest::get("https://example.org"));
    let (r1, r2) = (t1.await.unwrap(), t2.await.unwrap());
    println!("example.com = {}", r1.unwrap().status());
    println!("example.org = {}", r2.unwrap().status());
}
Run Code Online (Sandbox Code Playgroud)

或者我可以通过以下方式实现tokio::join!()

async fn v2() {
    let t1 = reqwest::get("https://example.com");
    let t2 = reqwest::get("https://example.org");
    let (r1, r2) = tokio::join!(t1, t2);
    println!("example.com = {}", r1.unwrap().status());
    println!("example.org = {}", r2.unwrap().status());
}
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,两个请求都是同时发生的。但是,在第二种情况下,两个请求在同一任务中运行,因此在同一线程上运行。

所以,我的问题是:

  • tokio::join!()超过有优势吗tokio::spawn()
  • 如果是的话,在什么场景下?(它与下载网页无关)

我猜生成一个新任务的开销非常小,但真的是这样吗?

Pet*_*all 21

差异取决于您配置运行时的方式。将在同一个任务中tokio::join!同时运行任务,同时为每个任务创建一个新任务。tokio::spawn

在单线程运行时,这些实际上是相同的。在多线程运行时,使用tokio::spawn!两次类似的操作可能会使用两个单独的线程。

文档tokio::join!

通过在当前任务上运行所有异步表达式,这些表达式可以并发运行,但不能并行运行。这意味着所有表达式都在同一线程上运行,如果一个分支阻塞该线程,则所有其他表达式将无法继续。如果需要并行性,请使用 生成每个异步表达式tokio::spawn并将连接句柄传递给join!

对于 IO 密集型任务,例如下载网页,您不会注意到差异;大部分时间将花费在等待数据包上,并且每个任务可以有效地交错其处理。

tokio::spawn当任务更多地受 CPU 限制并且可能相互阻塞时使用。

  • 我认为@kmdreko 的答案可能涵盖了这一点,所以我不会在我自己的答案中重复它。将数据移动到另一个线程的可能性引入了可能受到限制的约束,即“发送+‘静态’”。`join!` 在这方面更加灵活。 (2认同)

kmd*_*eko 13

我通常会从另一个角度来看待这个问题;我为什么要使用tokio::spawnover tokio::join?生成一个新任务比加入两个 future 有更多的限制,这个'static要求可能非常烦人,因此不是我的首选。

除了产生任务的成本(我认为这是相当微不足道的)之外,还有在原始任务完成时发出信号的成本。我也认为这是微不足道的,但您必须在您的环境和异步工作负载中测量它们,看看它们是否确实产生影响。

但你是对的,使用两个任务的最大好处是它们有机会并行工作,而不仅仅是同时工作。但另一方面,async它最适合 I/O 密集型工作负载,其中存在大量等待,并且根据您的工作负载,缺乏并行性可能不会产生太大影响,具体取决于您的工作负载。

总而言之,tokio::join它使用起来更好、更灵活,我怀疑技术差异会对性能产生影响。但一如既往:测量!

  • 作为 Tokio 维护者,我不太同意这个答案。除非您要加入两个小操作,否则即使在单线程运行时,生成也可能表现得更好。 (2认同)