将控制权让渡给运行时
回顾 “我们的第一个异步程序” 一节,在每个 await 点,如果被等待的 future 尚未就绪,Rust 会给运行时一个机会来暂停当前 任务并切换到另一个任务。反过来也同样成立:Rust 只会在 await 点 暂停异步代码块并将控制权交还给运行时。await 点之间的所有内容都是同步的。
这意味着,如果你在一个异步代码块中做了一大堆工作而没有任何 await 点, 那么该 future 会阻止其他所有 future 取得进展。你有时可能会听到 这被称为一个 future 饿死了(starving)其他 future。在某些情况下, 这可能没什么大不了的。然而,如果你正在进行某种开销昂贵的 初始化或长时间运行的工作,或者你有一个会无限期持续执行某项 特定任务的 future,你就需要考虑何时何地将控制权交还给运行时。
让我们模拟一个长时间运行的操作来说明饿死问题,
然后探讨如何解决它。示例 17-14 引入了一个 slow 函数。
extern crate trpl; // required for mdbook test
use std::{thread, time::Duration};
fn main() {
trpl::block_on(async {
// We will call `slow` here later
});
}
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
thread::sleep 模拟慢速操作这段代码使用 std::thread::sleep 而不是 trpl::sleep,因此调用
slow 会将当前线程阻塞若干毫秒。我们可以
用 slow 来代替现实世界中既长时间运行又
阻塞的操作。
在示例 17-15 中,我们使用 slow 来模拟在
一对 future 中执行这类 CPU 密集型的工作。
extern crate trpl; // required for mdbook test
use std::{thread, time::Duration};
fn main() {
trpl::block_on(async {
let a = async {
println!("'a' started.");
slow("a", 30);
slow("a", 10);
slow("a", 20);
trpl::sleep(Duration::from_millis(50)).await;
println!("'a' finished.");
};
let b = async {
println!("'b' started.");
slow("b", 75);
slow("b", 10);
slow("b", 15);
slow("b", 350);
trpl::sleep(Duration::from_millis(50)).await;
println!("'b' finished.");
};
trpl::select(a, b).await;
});
}
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
slow 函数来模拟慢速操作每个 future 只有在执行完一堆慢速操作之后才会将控制权交还给 运行时。如果你运行这段代码,将会看到如下输出:
'a' started.
'a' ran for 30ms
'a' ran for 10ms
'a' ran for 20ms
'b' started.
'b' ran for 75ms
'b' ran for 10ms
'b' ran for 15ms
'b' ran for 350ms
'a' finished.
与示例 17-5 中我们使用 trpl::select 来竞争获取两个
URL 的 future 一样,select 仍然会在 a 完成后立即结束。不过,
两个 future 中对 slow 的调用之间并没有任何交错。a future
会一直执行它的所有工作,直到 trpl::sleep 调用被 await,然后 b future
会一直执行它的所有工作,直到它自己的 trpl::sleep 调用被 await,最后
a future 完成。要让两个 future 都能在各自的慢速
任务之间取得进展,我们需要 await 点,以便将控制权交还给运行时。这
意味着我们需要某个可以 await 的东西!
在示例 17-15 中我们已经可以看到这种交接的发生:如果我们
移除 a future 末尾的 trpl::sleep,那么它会在 b future完全没有运行
的情况下直接完成。让我们尝试以 trpl::sleep 函数为起点,
让各个操作轮流取得进展,如示例 17-16 所示。
extern crate trpl; // required for mdbook test
use std::{thread, time::Duration};
fn main() {
trpl::block_on(async {
let one_ms = Duration::from_millis(1);
let a = async {
println!("'a' started.");
slow("a", 30);
trpl::sleep(one_ms).await;
slow("a", 10);
trpl::sleep(one_ms).await;
slow("a", 20);
trpl::sleep(one_ms).await;
println!("'a' finished.");
};
let b = async {
println!("'b' started.");
slow("b", 75);
trpl::sleep(one_ms).await;
slow("b", 10);
trpl::sleep(one_ms).await;
slow("b", 15);
trpl::sleep(one_ms).await;
slow("b", 350);
trpl::sleep(one_ms).await;
println!("'b' finished.");
};
trpl::select(a, b).await;
});
}
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
trpl::sleep 让各个操作轮流取得进展我们在每次调用 slow 之间都添加了带 await 点的 trpl::sleep 调用。
现在这两个 future 的工作交错进行了:
'a' started.
'a' ran for 30ms
'b' started.
'b' ran for 75ms
'a' ran for 10ms
'b' ran for 10ms
'a' ran for 20ms
'b' ran for 15ms
'a' finished.
a future 在将控制权交接给 b 之前仍会运行一小段时间,因为
它在调用 trpl::sleep 之前就调用了 slow,但在此之后,每当其中一个
future 碰到 await 点时,两个 future 就会来回切换。在本例中,我们
在每次调用 slow 之后都这样做,但我们也可以用任何
对我们最有意义的方式来拆分工作。
不过,我们并不是真的想在这里睡眠:我们想尽可能快地
取得进展。我们只是需要将控制权交还给运行时。我们可以
直接使用 trpl::yield_now 函数来做到这一点。在示例 17-17 中,我们将
所有那些 trpl::sleep 调用替换为 trpl::yield_now。
extern crate trpl; // required for mdbook test
use std::{thread, time::Duration};
fn main() {
trpl::block_on(async {
let a = async {
println!("'a' started.");
slow("a", 30);
trpl::yield_now().await;
slow("a", 10);
trpl::yield_now().await;
slow("a", 20);
trpl::yield_now().await;
println!("'a' finished.");
};
let b = async {
println!("'b' started.");
slow("b", 75);
trpl::yield_now().await;
slow("b", 10);
trpl::yield_now().await;
slow("b", 15);
trpl::yield_now().await;
slow("b", 350);
trpl::yield_now().await;
println!("'b' finished.");
};
trpl::select(a, b).await;
});
}
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
yield_now 让各个操作轮流取得进展这段代码不仅更清晰地表达了实际意图,而且可能比使用 sleep 快得多,
因为 sleep 所使用的这类定时器通常在精度上有所限制。
例如,我们使用的这个 sleep 版本,即使我们给它传入一
纳秒的 Duration,它也总是会至少睡眠一毫秒。再次强调,现代计算机非常快:
它们在一毫秒内可以做很多事!
这意味着异步甚至可以用于计算密集型任务,这取决于 你的程序还在做什么,因为它提供了一个有用的工具来 组织程序中不同部分之间的关系(但代价是异步状态机的开销)。这是一种 协作式多任务(cooperative multitasking),其中每个 future 都有权决定 何时通过 await 点交出控制权。因此,每个 future 也有 责任避免阻塞太长时间。在一些基于 Rust 的嵌入式 操作系统中,这是唯一的多任务形式!
当然,在真实代码中,你通常不会在每一行都交替进行函数调用和 await 点。虽然以这种方式让渡控制权相对廉价, 但它并不是免费的。在许多情况下,试图拆分一个 计算密集型任务反而可能使其明显变慢,因此有时为了整体性能, 让一个操作短暂阻塞反而更好。请始终通过 测量来确认你的代码真正的性能瓶颈在哪里。不过,如果你确实看到 大量你本以为会并发执行的工作却在串行发生,那么牢记 这背后的动态机制就很重要!
构建我们自己的异步抽象
我们还可以将多个 future 组合在一起,创建新的模式。例如,我们可以
用已有的异步构建块构建一个 timeout 函数。完成
后,其结果又将成为一个新的构建块,可用于创建
更多的异步抽象。
示例 17-18 展示了我们期望这个 timeout 如何与一个慢速
future 协同工作。
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
"Finally finished"
};
match timeout(slow, Duration::from_secs(2)).await {
Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
});
}
timeout 在有时间限制的情况下运行一个慢速操作让我们来实现它!首先,让我们考虑一下 timeout 的 API:
- 它本身需要是一个异步函数,这样我们才能 await 它。
- 它的第一个参数应该是要运行的 future。我们可以将其设为泛型, 使其能够处理任何 future。
- 它的第二个参数是要等待的最长时间。如果我们使用
Duration, 就可以方便地将其传递给trpl::sleep。 - 它应该返回一个
Result。如果 future 成功完成,Result将是Ok,其中包含该 future 产生的值。如果超时 先到期,Result将是Err,其中包含超时 所等待的时长。
示例 17-19 展示了这个声明。
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
"Finally finished"
};
match timeout(slow, Duration::from_secs(2)).await {
Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
});
}
async fn timeout<F: Future>(
future_to_try: F,
max_time: Duration,
) -> Result<F::Output, Duration> {
// Here is where our implementation will go!
}
timeout 的签名这满足了我们对类型的目标。现在让我们思考一下所需的行为:
我们希望让传入的 future 与给定的时长进行竞争。我们可以使用
trpl::sleep 从该时长创建一个定时器 future,并使用 trpl::select
让该定时器与调用者传入的 future 一起运行。
在示例 17-20 中,我们通过匹配 await
trpl::select 的结果来实现 timeout。
extern crate trpl; // required for mdbook test
use std::time::Duration;
use trpl::Either;
// --snip--
fn main() {
trpl::block_on(async {
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
"Finally finished"
};
match timeout(slow, Duration::from_secs(2)).await {
Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
});
}
async fn timeout<F: Future>(
future_to_try: F,
max_time: Duration,
) -> Result<F::Output, Duration> {
match trpl::select(future_to_try, trpl::sleep(max_time)).await {
Either::Left(output) => Ok(output),
Either::Right(_) => Err(max_time),
}
}
select 和 sleep 定义 timeouttrpl::select 的实现并不公平:它总是按照参数
传入的顺序轮询它们(其他 select 实现会
随机选择先轮询哪个参数)。因此,我们将 future_to_try
先传给 select,这样即使 max_time 是非常
短的时长,它也有机会完成。如果 future_to_try 先完成,select 将返回 Left
并携带 future_to_try 的输出。如果 timer 先完成,select 将
返回 Right 并携带定时器的输出 ()。
如果 future_to_try 成功并且我们得到一个 Left(output),我们就返回
Ok(output)。如果睡眠定时器先到期并且我们得到一个 Right(()),我们
就用 _ 忽略那个 (),转而返回 Err(max_time)。
至此,我们就用另外两个异步辅助函数构建出了一个可用的 timeout。如果
我们运行这段代码,它将在超时后打印失败信息:
Failed after 2 seconds
由于 future 可以与其他 future 组合,因此你可以使用较小的 异步构建块构建出非常强大的工具。例如,你可以使用同样的 方法将超时与重试结合起来,进而将它们用于 诸如网络调用之类的操作(如示例 17-5 中的那些)。
在实践中,你通常会直接使用 async 和 await,
其次才是使用诸如 select 这样的函数和诸如 join!
这样的宏来控制最外层 future 的执行方式。
到目前为止,我们已经了解了多种同时处理多个 future 的方式。 接下来,我们将看看如何通过流(stream)随时间按序列 处理多个 future。