80、Asynchronous functions(异步函数)

异步函数

我们目前编写的所有函数和方法都是“急切型”的。
在我们调用它们之前,它们什么都不会发生。但一旦调用,它们就会一直运行到完成:完成所有工作,然后返回结果。

有时,这种做法并不理想。
例如,如果我们正在编写一个 HTTP 服务器,可能会有很多等待:等待请求体到达、等待数据库响应、等待下游服务响应等等。

如果在等待期间可以做其他事情呢?
如果可以选择在计算过程中途放弃呢?
如果可以选择优先执行其他任务而不是当前任务呢?

这就是异步函数的用武之地。

async fn

我们可以使用 async 关键字来定义异步函数:

use tokio::net::TcpListener;

// 这个函数是异步的
async fn bind_random() -> TcpListener {
    // [...]
}

如果像调用普通函数一样调用 bind_random 会发生什么?

fn run() {
    // 调用 `bind_random`
    let listener = bind_random();
    // 执行到这里呢?
}

什么都没发生!
Rust 不会在你调用 bind_random 时立即执行它,即使是作为后台任务也不会(这与我们基于其他语言经验的预期不同)。Rust 中的异步函数是惰性的:它们不会执行任何操作,直到我们明确地请求它们执行。用 Rust 的术语来说,bind_random 返回的是一个 Future,它代表一个可能稍后完成的计算。之所以称之为 Future,是因为它们实现了 Future trait,我们将在本章后面详细学习这个接口。

.await

让异步函数执行某些操作的最常用方法是使用 .await 关键字:

use tokio::net::TcpListener;

async fn bind_random() -> TcpListener {
    // [...]
}

async fn run() {
    // 调用 `bind_random` 并等待其完成
    let listener = bind_random().await;
    // 现在 `listener` 已准备就绪
}

.await 不会将控制权返回给调用者,直到异步函数运行完成为止——例如,直到上面的示例中创建了 TcpListener 为止。

Runtimes

到这里也挺困惑的,我们刚才说了异步函数的优势在于它们不会一次性完成所有工作。然后我们又介绍了 .await,它会在异步函数执行完毕之前一直保持等待状态。这岂不是又把我们原本想要解决的问题重新引入了?这到底有什么意义呢?

并非如此!调用 .await! 时,幕后会发生很多事情。 我们实际上是将控制权交给了异步运行时async executor,也称为异步执行器async exector。执行器才是真正发挥作用的地方:它们负责管理所有正在进行的异步任务。具体来说,它们需要平衡两个不同的目标:

  • Progress:他们会确保各项任务尽可能取得进展。
  • Efficiency:如果某项任务需要等待某个条件,他们会尽量确保其他任务在此期间可以运行,从而充分利用可用资源。

没有默认运行时

Rust 在异步编程方面相当独特:它没有默认的运行时库。标准库中也没有提供。我们需要自行选择!

大多数情况下,我们会从生态系统中选择一个运行时库。有些运行时库设计得比较通用,适用于大多数应用场景。tokio 和 async-std 就属于这一类。其他运行时库则针对特定用例进行了优化,例如 Embassy 专为嵌入式系统而设计。

在本课程中,我们将使用 tokio,它是 Rust 中最流行的通用异步编程运行时库。

#[tokio::main]

可执行文件的入口点,也就是主函数,必须是一个同步函数(synchronous function)。我们需要在这里设置并启动我们选择的异步运行时。

大多数运行时都提供了一个宏来简化这个过程。例如,对于 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] 宏来简化此操作:

#[tokio::test]
async fn my_test() {
    // 这里放置异步测试代码
}

 

阅读剩余
THE END