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



假设我想与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());
}

或者我可以用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());
}

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

我的问题是:

  • tokio::join!()是否优于tokio::spawn()?
  • 如果是,在哪些场景下?(它不需要做任何下载网页)

我猜有一个非常小开销产卵一个新的任务,但这是吗?

差异将取决于您如何配置运行时。tokio::join!将在同一个任务中并发运行任务,而tokio::spawn将为每个任务创建一个新任务。

在单线程运行时中,它们实际上是相同的。在多线程运行时中,像这样两次使用tokio::spawn!可以使用两个独立的线程。

From docs fortokio::join!:

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

对于io绑定任务,比如下载网页,你不会注意到差异;大部分时间将用于等待数据包,并且每个任务可以有效地交错处理。

使用tokio::spawn时,任务的cpu较多,可能会阻塞彼此。

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

除了生成任务的成本,我猜这是相当微不足道的,还有在原始任务完成时发出信号的成本。我也猜这是边际的,但是你必须在你的环境和异步工作负载中测量它们,看看它们是否真的有影响。

但是你是对的,使用两个任务的最大好处是它们有机会并行工作,而不仅仅是并发地工作。但另一方面,async最适合有大量等待的I/o负载,并且根据您的工作负载,缺乏并行性可能不太可能产生太大影响。

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

@kmdreko的回答很棒,我想添加一些细节!

如前所述,使用tokio::spawn'static的要求,所以下面的代码片段不能编译:
async fn v1() {
let url = String::from("https://example.com");
let t1 = tokio::spawn(reqwest::get(&url)); // `url` does not live long enough
let t2 = tokio::spawn(reqwest::get(&url));
let (r1, r2) = (t1.await.unwrap(), t2.await.unwrap());
}

但是,带tokio::join!的等效代码段可以编译为:

async fn v2() {
let url = String::from("https://example.com");
let t1 = reqwest::get(&url);
let t2 = reqwest::get(&url);
let (r1, r2) = tokio::join!(t1, t2);
}

同样,这个答案让我对生成一个新任务的成本感到好奇,所以我编写了以下简单的基准测试:

use std::time::Instant;
#[tokio::main]
async fn main() {
let now = Instant::now();
for _ in 0..100_000 {
v1().await;
}
println!("tokio::spawn = {:?}", now.elapsed());
let now = Instant::now();
for _ in 0..100_000 {
v2().await;
}
println!("tokio::join! = {:?}", now.elapsed());
}
async fn v1() {
let t1 = tokio::spawn(do_nothing());
let t2 = tokio::spawn(do_nothing());
t1.await.unwrap();
t2.await.unwrap();
}
async fn v2() {
let t1 = do_nothing();
let t2 = do_nothing();
tokio::join!(t1, t2);
}
async fn do_nothing() {}

在发布模式下,我在macOS笔记本电脑上得到以下输出:

tokio::spawn = 862.155882ms
tokio::join! = 369.603µs

编辑:这个基准在很多方面都有缺陷(见注释),所以不要依赖它来获得具体的数字。然而,刷出比加入两个任务更昂贵的结论似乎是正确的。

相关内容

  • 没有找到相关文章

最新更新