异步函数
迄今为止,你编写的全部函数和方法都是即时执行的。除非你调用它们,否则什么都不会发生。但一旦调用了,它们就会运行至完成:它们会做所有的工作,然后返回输出结果。
有时这并非理想情况。例如,如果你正在编写一个HTTP服务器,可能会有很多等待的情况:等待请求体到达、等待数据库响应、等待下游服务回复等。
如果在等待时可以做些其他事情会怎样?如果你可以选择中途放弃计算会怎样?如果你可以选择优先处理另一个任务而非当前任务会怎样?
这就是异步函数发挥作用的地方。
async fn
你可以使用async
关键字来定义一个异步函数:
#![allow(unused)] fn main() { use tokio::net::TcpListener; // 这个函数是异步的 async fn bind_random() -> TcpListener { // [...] } }
如果你像调用普通函数那样调用bind_random
会发生什么?
#![allow(unused)] fn main() { fn run() { // 调用 `bind_random` let listener = bind_random(); // 现在怎么办? } }
什么也不会发生!当你调用bind_random
时,Rust并不会开始执行它,甚至不会作为一个后台任务来启动(这可能基于你在其他语言中的经验)。Rust中的异步函数是惰性的:它们直到你明确要求它们执行才开始做任何工作。使用Rust的术语来说,我们说bind_random
返回了一个未来,这是一种代表可能稍后完成的计算的类型。它们之所以称为“未来”,是因为它们实现了Future
特质,我们将在本章后面详细探讨这个接口。
.await
让异步函数执行一些工作的最常见方法是使用.await
关键字:
#![allow(unused)] fn main() { use tokio::net::TcpListener; async fn bind_random() -> TcpListener { // [...] } async fn run() { // 调用 `bind_random` 并等待它完成 let listener = bind_random().await; // 现在 `listener` 已经准备好了 } }
.await
直到异步函数运行完成——例如,上述示例中直到TcpListener
被创建——才会将控制权交还给调用者。
运行时
如果你感到困惑,这是很正常的!我们刚刚说过异步函数的优点之一是它们不会立即做所有工作。然后我们介绍了.await
,它要等到异步函数完成才会返回。我们是不是又重新引入了我们试图解决的问题?意义何在?
不尽然!在你调用.await
时,幕后发生了许多事情!你将控制权交给了一异步运行时,也称为异步执行器。执行器是魔法发生的地点:它们负责管理你所有的正在进行的任务。具体来说,它们平衡两个不同的目标:
- 进展:确保任务在可能时取得进展。
- 效率:如果一个任务在等待某事,它们尽量确保另一个任务可以在此期间运行,充分利用可用资源。
无默认运行时
Rust在异步编程方面采取的方法相当独特:没有默认运行时。标准库不附带运行时。你需要自己引入!
在大多数情况下,你会从生态系统中选择一个可用的选项。有些运行时设计得广泛适用,对大多数应用程序都是坚实的选择。tokio
和async-std
属于这一类。其他运行时则针对特定用例进行了优化,例如,embassy
针对嵌入式系统。
在整个课程中,我们将依赖于tokio
,这是Rust中通用异步编程最受欢迎的运行时。
#[tokio::main]
你的可执行文件的入口点,main
函数,必须是一个同步函数。你应该在这里设置并启动你选择的异步运行时。
大多数运行时都提供了一个宏来简化这个过程。对于tokio
,它是tokio::main
:
#[tokio::main] async fn main() { // 你的异步代码放这里 }
这展开为:
fn main() { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on( // 你的异步函数放这里 // [...] ); }
#[tokio::test]
测试也是如此:它们必须是同步函数。每个测试函数都在自己的线程中运行,如果你需要在测试中运行异步代码,你需要负责设置并启动异步运行时。tokio
提供了一个#[tokio::test]
宏来简化这一过程:
#![allow(unused)] fn main() { #[tokio::test] async fn my_test() { // 你的异步测试代码放这里 } }