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