Rust异步编程简介

Rust异步编程简介

计算机已经尽可能快了。加快程序速度的一种方法是并行或并发执行操作。这两个术语之间有细微的区别。并行执行意味着我们同时在两个不同的 CPU 上执行两个不同的任务。并发执行意味着单个 CPU 通过交错执行这些任务,同时在多个任务上取得进展。

Rust 标准库为底层操作系统提供了绑定和抽象。这包括线程,一种并行运行代码的方式。并行性由操作系统管理,您可以拥有与CPU核心一样多的线程,但也可以有更多,并且操作系统决定何时执行什么。这可能非常繁重并且有大量开销。

因此,我们陷入了两种方法:要么按顺序运行所有内容,要么使用操作系统线程并行执行,这可能会导致开销。对于某些领域(例如 Web 或网络应用程序 ,即IO密集型)来说,它们都可能不是最佳解决方案。

异步试图解决这些问题。异步是一种顺序编写代码但同时并发执行代码的方法,无需管理任何线程或执行。这个想法是 将现有代码分割成任务,然后执行一部分代码,并让异步运行时选择下一个需要执行的任务。然后,运行时决定何时执行什么,并且可以以非常有效的方式执行

它还利用了这样一个事实:大多数时候,CPU 正在等待某些事情发生,例如网络请求或要读取的文件。看下面的代码行。

rust 复制代码
let mut socket = net::TcpStream::connect((host, port)).unwrap();

我们所做的就是建立一个 TCP 连接。但这需要时间。对于您来说不一定引人注目,但对于计算机来说,这意味着什么都不做,只是等待建立连接。 其实 我们可以更好地利用这段时间。

异步原语

并发执行在编程领域并不是什么新鲜事。此外,异步编程已经存在了一段时间,您可能在 JavaScript 或 C# 中看到过类似的东西。但在 Rust 中,乍一看事情可能很相似,但如果我们仔细观察就会有所不同。

一个很大的区别是 Rust 没有异步运行时。我们需要一个异步运行时来管理任务的正确执行,但参与的 Rust 团队认为不存在"一刀切"的异步运行时,开发人员应该有权选择适合自己需求的运行时。从概念上讲,这不同于例如Go,它只有一种并发模型:goroutines。开发人员却陷入困境。

在 Rust 中,我们可以决定使用哪一种。尽管如此,Rust 为我们提供了一种为异步执行器准备任务的方法。这是通过使用 Future trait 的抽象来完成的。 Future trait是 Rust 异步编程的核心。它是一种trait,代表一种尚不可用但在未来某个时候可用的值。这与 JavaScript 中的 Promise 非常相似。

实现 Future 的所有内容都可以在异步运行时中执行。 Future trait定义如下:

rust 复制代码
pub trait Future {
    type Output;
    // Required method
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

这很简单。它有一个关联类型 Output ,它代表了未来的值。它有一个名为 poll 的方法,它带有 Context 并返回 Poll

Poll 是一个具有两种状态的枚举。要么 Pending ,这意味着我们等待一个值。或者 Ready ,表示该值可用。 Ready 变体保存 Output 类型的输出。

rust 复制代码
pub enum Poll<T> {
    Ready(T),
    Pending,
}

Context 目前仅用于提供对 Waker 对象的访问。 Waker 是告诉运行时再次轮询此任务所必需的。

好吧好吧,那是什么?Polling,waking?让我们深入挖掘一下。

执行

如前所述, Future trait用于抽象可以在异步运行时中执行的任务。但这是如何运作的呢?

公平地说,详细来说,这取决于所使用的异步运行时,但一些基本概念对于所有这些都是相同的。

尼克·卡梅伦 (Nick Cameron) 撰写了有关此主题的概述,总结如下:

异步运行时有一个执行器。执行器通常有两个关键 API: spawnblock_onblock_on 用于 阻塞等待当前线程上的任务完成。 spawn 用于在执行器上启动一个新任务,但非阻塞。它立即返回。返回值取决于 Future 。是否有异步发生?然后轮询 Future 将立即返回 Poll::Pending ,同时还为执行器设置规则,以便在任务准备好时唤醒任务。这可以是操作系统上的一些 IO 事件,例如已建立的 TCP 连接。如果没有任何异步发生, Future 将返回 Poll::Ready 及其返回值。

一旦事件发生,waker 指示执行器再次轮询相同的future,可能已经有结果了。

语法糖: async and await

好的,所以您需要的只是实现 Future 的函数或结构,然后您就完成了。这可行吗?嗯,从字面上看,这并不那么容易。实现 Future trait可能非常艰巨,而且不太符合工效学。

这就是 Rust 引入 asyncawait 关键字的原因。要使某些内容异步,它需要返回 Future 。因此,如果您想要一个方法 read_to_string ,这是同步版本:

rust 复制代码
fn read_to_string(&mut self, buf: &mut String) -> Result<usize>;

异步版本如下所示:

rust 复制代码
fn read_to_string(&mut self, buf: &mut String) -> impl Future<Output = Result<usize>>;

这儿有个有语法糖。您可以将其声明为 async ,而不是返回 Future

rust 复制代码
async fn read_to_string(&mut self, buf: &mut String) -> Result<usize>;

您也不需要自己进行poll 。您可以使用 await 关键字等待 Future 的结果。

rust 复制代码
let result = fileread_to_string(&mut buf).await;

在幕后,Rust 编译器 为您创建了Future。它通过 将代码拆分为多个任务来实现这一点每个 await 都是分隔任务的分割点 。然后,编译器会为您创建一个状态机,并为您实现 Future trait 。对于每个 await ,状态机都会被轮询并可能移动到下一个状态。 Tokio 团队在本教程中精彩地展示了how those Futures can be implemented or created by the compiler in this tutorial

Tokio 是最流行的运行时之一,专为网络应用程序的异步执行而设计。它也是早期异步的playground,并且很稳定,可用于生产,而且很可能也是您正在使用的任何 Web 框架的基础。它不仅提供了操作系统事件的必要抽象,还提供了具有不同模式的功能丰富的运行时,以及标准库 IO 和网络功能的异步表示。如果你想开始使用 Rust 中的异步,Tokio 是一个不错的选择。

Traits 中的async 方法

所有这些都导致了异步Rust中最需要的,也是最期待的特性之一:在trait中定义async 方法。该功能最近已在 Rust 中引入,但仍然存在一些限制 。下面的问题是,我们作为开发人员希望使用漂亮的 async / await 语法进行编写,但编译器需要为自动生成的代码准备 Future trait实现状态机,这可能会变得非常复杂

让我们看一个例子,它想要为聊天应用程序定义一个编写接口,名为 ChatSink 。这就是我想写的。

rust 复制代码
pub trait ChatSink {
  type Item: Clone;
  async fn send_msg(&mut self, msg: Self::Item) -> Result<(), ChatCommErr>;
}

一旦我们想将其转换为使用 Future 实现的东西,事情就会变得有点棘手。我们需要定义一个 Future 返回类型,而不是 async 方法,但我们不知道它会是哪个 Future !这将由trait 的实现者在稍后阶段定义。所以我们能做的就是说无论发生什么,它都会实现 Future trait 。这是通过使用 impl 关键字来完成的。

rust 复制代码
pub trait ChatSink {
  type Item: Clone;
  fn send_msg(&mut self, msg: Self::Item) -> impl Future<Output = Result<(), ChatCommErr>>;
}

但有趣的是 impl Trait 也只是关联类型的语法糖。事实上,会产生类似这样的东西。

rust 复制代码
pub trait ChatSink {
  type Item: Clone;
  type $: Future<Output = Result<(), ChatCommErr>>;
  fn send_msg(&mut self, msg: Self::Item) -> Self::$;
}

但这还不是全部,我们遗漏了一个非常重要的细节。与其他 impl Trait 解决方法相比, Future 需要添加生命周期参数。这与 future 内部的处理方式有关:它们不执行代码,它们只是将执行代码的机会传递给另一个运行时环境,即我们之前提到的执行器!异步函数创建这样的 future,并且它们需要保留对 输入参数的所有引用。根据 Rust 的所有权规则,所有这些引用都需要与future本身一样长久。为了确保此信息可用,我们需要向 Future trait添加一个生命周期参数

这就产生了一个称为 通用关联类型的功能。 ChatSink 特征的等效版本如下所示:

rust 复制代码
pub trait ChatSink {
  type Item: Clone;
  type $<'m>: Future<Output = Result<(), ChatCommErr>> + 'm;
  fn send_msg(&mut self, msg: Self::Item) -> Self::$<'_>;
}

但在 Rust 1.75 之前,这一切都是不可能的 。这已经发生了变化,但仍然存在一些限制。 impl Trait 目前不允许添加用户定义的trait 约束(trait bounds),如果您作为开发人员想要实现库中的trait ,则该功能是必需的。更不用说,如果您想决定异步代码只能在单线程或多线程运行时上工作,那么添加 SendSync 标记特征就是您想要的自行定义(更多关于这些标记特征的信息请参见here)。

为什么我需要知道所有这些?

公平地说,这是很多信息,并且深入了解了异步 Rust 的本质细节。但这是有原因的。就像 Rust 中的一切一样,事情一开始看起来简单明了,但一旦你深入挖掘,你会发现有很多复杂性和很多需要考虑的事情

Rust 中的异步编程也会发生同样的情况。您肯定已经完成了定义异步方法。毕竟,您正在阅读 Shuttle 博客,并且异步为 Rust 中的 Web 开发提供了动力。起初,它们很简单,但突然间您可能会看到无法掌握的错误消息

您在异步函数中定义一个资源,将其包装在 std::sync::Mutex 中,并在锁定它后获取它的 MutexGuard 。突然您决定调用异步 API 并传递 .await 点。编译器会对你抱怨,因为 MutexGuard 没有实现 Send 特征 ,并且你不能将它传递给另一个线程。但为什么需要将它传递给另一个线程呢?您所做的只是调用异步函数?这就是运行时的用武之地。您的运行时配置可能是多线程工作的,并且您永远不知道哪个工作线程执行当前任务 。由于您需要为自动 Future 实现准备好所有资源,因此所有这些引用和资源都需要是线程安全的。还有更多的陷阱,但这是另一次的事情了。

进一步阅读

如果您已经了解了这么多,您可能会对以下资源感兴趣:


Async Rust in a Nutshell

相关推荐
摸鱼的春哥5 分钟前
春哥的Agent通关秘籍07:5分钟实现文件归类助手【实战】
前端·javascript·后端
Smart-Space5 分钟前
htmlbuilder - rust灵活构建html
rust·html
魔力军15 分钟前
Rust学习Day2: 变量与可变性、数据类型和函数和控制流
开发语言·学习·rust
Victor35621 分钟前
MongoDB(2)MongoDB与传统关系型数据库的主要区别是什么?
后端
JaguarJack22 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端·php·服务端
BingoGo23 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端
Victor35624 分钟前
MongoDB(3)什么是文档(Document)?
后端
牛奔2 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌7 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
KYGALYX9 小时前
服务异步通信
开发语言·后端·微服务·ruby