问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

Rust异步编程简介

创作时间:
作者:
@小白创作中心

Rust异步编程简介

引用
CSDN
1.
https://blog.csdn.net/xuejianxinokok/article/details/136468691

异步编程是现代软件开发中的一个重要概念,特别是在处理I/O密集型任务时。Rust语言通过其独特的异步编程模型,为开发者提供了强大的工具来编写高效、并发的代码。本文将深入探讨Rust异步编程的核心概念、实现机制以及实际应用,帮助读者理解并掌握这一重要技术。

Rust异步编程简介

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

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

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

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

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

let mut socket = net::TcpStream::connect((host, port)).unwrap();

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

异步原语

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

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

在Rust中,我们可以决定使用哪一种。尽管如此,Rust为我们提供了一种为异步执行器准备任务的方法。这是通过使用Futuretrait的抽象来完成的。

Futuretrait是Rust异步编程的核心。它是一种trait,代表一种尚不可用但在未来某个时候可用的值。这与JavaScript中的Promise非常相似。

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

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类型的输出。

pub enum Poll<T> {
    Ready(T),
    Pending,
}

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

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

执行

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

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

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

异步运行时有一个执行器。执行器通常有两个关键API:spawnblock_on

block_on用于阻塞等待当前线程上的任务完成。

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

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

语法糖:asyncandawait

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

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

fn read_to_string(&mut self, buf: &mut String) -> Result<usize>;

异步版本如下所示:

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

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

async fn read_to_string(&mut self, buf: &mut String) -> Result<usize>;

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

let result = fileread_to_string(&mut buf).await;

在幕后,Rust编译器为您创建了Future。它通过将代码拆分为多个任务来实现这一点每个await都是分隔任务的分割点。然后,编译器会为您创建一个状态机,并为您实现Futuretrait。对于每个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语法进行编写,但编译器需要为自动生成的代码准备Futuretrait实现状态机,这可能会变得非常复杂

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

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

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

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

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

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本身一样长久。为了确保此信息可用,我们需要向Futuretrait添加一个生命周期参数

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

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实现准备好所有资源,因此所有这些引用和资源都需要是线程安全的。还有更多的陷阱,但这是另一次的事情了。

进一步阅读

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

  • Nick Cameron writes a lot about Async Rust, you might want to check it out.
  • So doesWithout Boats, who gives a lot of insights into the design and development of async Rust.
  • TheTokio Tutorial on Async in Depthis nothing but excellent.
  • So is the Async chapter in“Programming Rust”by Jim Blandy, Jason Orendorff, and Leonora Tindall.
  • Also check out my talk at the firstShuttle Labs.
    Async Rust in a Nutshell
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号