假设我想和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()有优势吗?我猜产生一项新任务的开销很小,但这是吗?
发布于 2021-10-20 03:01:59
我通常会从另一个角度来看待这个问题;为什么我要在tokio::join上使用tokio::join?产生一个新的任务比加入两个期货有更多的限制,'static的要求可能是非常恼人的,因此不是我的选择。
除了产生任务的成本,我想这是相当微不足道的,还有当它完成的时候发出信号的成本。我也认为这是边际的,但是您必须在您的环境和异步工作负载中度量它们,看看它们是否真的有影响。
但是你是对的,使用两个任务最大的好处是他们有机会并行工作,而不仅仅是同时工作。但是另一方面,async最适合于I/O绑定的工作负载,在那里有大量的等待,并且取决于您的工作负载,这种缺乏并行性可能不会产生很大的影响。
总之,tokio::join是一个更好和更灵活的使用,我怀疑技术上的差异会对性能产生影响。但一如既往:量入为出!
发布于 2021-10-20 02:51:21
差异将取决于您如何配置运行时。tokio::join!将在同一任务中并发运行任务,而tokio::spawn!则为每个任务创建一个新任务。
在单线程运行时,它们实际上是相同的。在多线程运行时,两次使用tokio::spawn! (类似于),可能使用两个单独的线程。
来自文档 for tokio::join!
通过在当前任务上运行所有异步表达式,这些表达式能够同时运行,但不能在并行中运行。这意味着所有表达式都运行在同一个线程上,如果一个分支阻塞该线程,则所有其他表达式都将无法继续。如果需要并行性,则使用
tokio::spawn生成每个异步表达式,并将连接句柄传递给join!。
对于IO绑定的任务,比如下载网页,您不会注意到其中的区别;大部分时间将用于等待数据包,并且每个任务都可以有效地交织它们的处理。
当任务受到更多的CPU限制,并且可能相互阻塞时,使用tokio::spawn!。
发布于 2021-10-20 03:46:45
@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编辑:这个基准在很多方面都有缺陷(见注释),所以不要依赖它来获取具体的数字。然而,关于产卵比加入2项任务更昂贵的结论似乎是正确的。
https://stackoverflow.com/questions/69638710
复制相似问题