发布于 2026-01-05 5 阅读
0

详解:Rust 中的异步是如何工作的?

详解:Rust 中的异步是如何工作的?

本文将概述 Rust 中异步编程的原理和实现方式。本文不会涉及并发、并行和异步代码之间的区别。

异步概念

异步编程是一种允许程序在等待特定操作结果时不阻塞工作流程的概念。因此,您可以打开一个大文件或查询数据库,但程序会继续处理后续的每一行数据。

这个概念最初是在内核层面提出的,因为用户希望能够在打字的同时听音乐。在软件层面,这是通过多线程实现的。在CPU层面,每个核心可以同时运行多个进程。

后来,Web 服务器应运而生,它们需要在执行 I/O 任务的同时维持数百万个连接。为了以非阻塞的方式实现这一点,我们可以使用内核级线程,或者实现我们自己的线程和事件处理方式。

需要什么以及为什么

内核已经实现了这个概念(通过线程和其他概念),但是它们非常“昂贵”,这意味着可用的资源是有限的,在操作系统层面处理这个问题会增加一个全新的复杂性。

因此,最好在程序层面处理内部异步流程。我们需要一个所谓的运行时环境,它可以处理异步代码并能够与内核通信。

总体思路是:

  • 在程序级别实现你自己的线程和队列处理方式(绿色线程)
  • 为你的语言添加语法糖,以便运行时/编译器能够识别代码中的异步部分。
  • 添加异步类型,以便它们在“完成”时发出通知。

异步概述

例如,异步类型需要具有特定的状态(处理中和完成),而不是像字符串那样需要处理其他类型。运行时可以处理这些类型并设置其状态。之后,您可以在代码中稍后访问该值,或者等待异步处理完成后再继续执行。

工作流程

在代码中,你可以将一个方法标记为异步(async)。在这个异步方法中,你可以使用异步类型。你可以等待它们完成(例如,“从 GitHub 获取数据……”),也可以“启动”它们,继续执行你的流程,稍后再检查它们是否完成并使用它们的值。

代码编写完成后,你需要一个运行时环境来处理代码中的异步部分并实际执行它。运行时环境还需要从队列中取出进程并将其交给操作系统,因为真正的工作是在操作系统中完成的。

操作系统完成处理后,会通知运行时,运行时会将异步类型内部的状态设置好,并将其交还给程序工作流。

NodeJS vs. Go vs. Rust

让我们来看看 Node、Go 和 Rust 是如何实现我们讨论过的概念的,即:语法、类型和运行时。

NodeJS

在 NodeJS 中,您可以使用 ` async/awaitand` 语法Promises。您可以创建await一个Promise`aka` 类型的操作,该操作可能需要更多时间来处理。

NodeJS 异步

const async_method = async () => {
    const dbResults = await dbQuery();
    const results = await serviceCall(dbResults);
    console.log(results);
}
Enter fullscreen mode Exit fullscreen mode

在 Go 语言中,你使用 `go start`goroutines而不是 `go start` 。你只需简单地写 `go start`Promises而不是 ` go start`。Go 语言自带 Go 运行时环境,而不是 V8。async/awaitgo method_name()

Go 异步

f(greeting string) {
    fmt.Println(greeting, ", World!")
}

go f("Hello")

Enter fullscreen mode Exit fullscreen mode

Rust 异步生态系统仍在发展中,尚未最终定型。这里的建议是使用 `and`async/await代替 ` Promisesand` ,这样Goroutines你就有了Futures`。

Rust 语言团队决定不包含任何运行时环境。Rust 的目标是尽可能精简,并能够根据需要随时替换组件。因此,你需要依赖 crate 来提供合适的运行时环境。

最流行的是tokio,它内部使用mio作为事件队列。其他运行时也在使用它,mio因为它提供了对诸如 epoll、kqueue 和 IOCP 等内核方法的抽象。

Rust 异步

Rust 的一个特殊之处在于,你必须“启动”一个 Future。仅仅像 NodeJS 中的 Promise 那样声明它,或者直接写入代码,go name_of_goroutine()并不会立即触发 Future 执行任何操作。因此,如果你使用的是 tokio,你需要:

let response = client.get("http://httpbin.org")

let response_is_ok = response
    .and_then(|resp| {
        println!("Status: {}", resp.status());
        Ok(())

tokio::run(response_is_ok);
Enter fullscreen mode Exit fullscreen mode

希望在不久的将来,你就可以async在 Rust 中这样使用它了:

#[async]
fn async_function_name(...) -> Result<ReturnType, ErrorType> {
    let db_results = await!(query_database());
    let more_data = await!(fetch_another_service(db_resukts));
    process(more_data)
}
Enter fullscreen mode Exit fullscreen mode

语法async/await仍在制定中,需要获得批准、合并,并且部分语言内容需要调整为新形式

Rust 异步详解

让我们更深入地了解一下运行时的工作原理:

Rust tokio 异步

Tokio内部使用了Reactor -Executor 模式

tokio 和其他运行时环境的目标是构建一个高度可扩展的服务器,以实现高原始数据吞吐量。它们不希望在执行 I/O 操作时发生阻塞。我们基本上有两种选择:基于线程的架构或事件驱动架构。简而言之:由于物理资源有限,基于线程的架构存在局限性。

因此,事件驱动在我们这种情况下是最佳选择。它会注册传入的 Future 请求,并保存指向异步函数处理程序的指针。然后,它会在内核中触发一个事件。一旦 I/O 操作完成,我们就调用该指针,并使用 I/O(内核)结果执行异步方法。

为此,我们需要一个 Reactor,用于通知是否有数据通过网络传入或文件写入操作正在进行中;还需要一个 executor,用于接收这些数据并执行异步函数(Future)。

此外,每个运行时都需要理解内核方法(例如epoll)来启动 I/O 操作。Rust 有一个名为mio 的crate实现了这些内核方法。Tokio 内部使用了 mio。

它能用吗?

目前异步 Rust 领域正发生着许多变化。要推出一个易于使用和理解的最终版本还需要一些时间。在此之前,您可以像往常一样使用您熟悉的 Web 框架,因为它们已经自带运行时环境。

例如,Dropbox 在生产环境中结合使用 Futurestokio来提供 Dropbox 服务器磁盘上的数据。Futures 在 Rust 稳定版中的版本为 0.1,在 Rust nightly 版中的版本为 0.3。运行时tokio依赖于 Rust 稳定版,因此使用的是 Futures 0.1。

你可以通过compat 模块将 0.3 转换为 0.1 Futures,反之亦然

Rust 还需要几个月的时间来完善其易用且强大的 Futures。与其他语言相比,Futures 的开销更低,而且你可以选择轻量级或厚重的运行时环境,完全取决于你。

开始使用

如前所述,Tokio 是您可以使用的运行时环境之一。另一种选择是RomioJuliex的组合。

如果您正在构建 Web 应用程序,可以使用名为hyper 的crate ,它已经包含了 tokio。因此,您可以在应用程序中使用 Futures 0.1。

保持更新

你可以访问areweasyncyet网站,了解异步 Rust 的最新进展。同样,arewewebyet 网站也在追踪用于构建 Web 应用程序的框架和工具的最新进展。

延伸阅读
  1. Tokio 和异步 I/O 究竟是什么?
  2. Rust 和 Haskell 中的 Futures
  3. NodeJS 中的快速异步
  4. 以 Goroutine 为例
  5. Tokio:构建运行时
文章来源:https://dev.to/gruberb/explained-how-does-async-work-in-rust-46f8