异步 Rust:基本概念
TL;DR:我将尝试对异步 Rust 周围的一些概念进行易于理解的说明:async、await、Future、Poll、Context、Waker、Executor 和 Reactor。
正如我在这里写的大多数内容一样,我们已经有一些与异步 Rust 相关的优秀内容。让我列举几个:
- Rust 中的异步编程,又名异步书;不完整,但很棒。
- 史蒂夫谈论了Rust 的 async/await 之旅以及它的工作原理。
- 没有 Boats提出的 await 语法(其他带有标签的条目
async
也Future
非常出色)。 - Jon 的关于Futures 和 async/await 如何工作的流。
既然有这么多精彩的信息,为什么还要写呢?我的答案和
几乎我的 DEV 博客上的每篇文章:都是为了吸引那些对这些内容仍然有点难以理解的读者。
所以,如果你想学习中级水平的内容,可以直接阅读上面列出的内容。否则,我们继续吧 :)
异步/.await
异步 Rust(简称 async Rust)是通过async/.await
语法来实现的。这意味着这两个关键字(async
和.await
)是编写异步 Rust 的核心。那么,什么是异步 Rust?
异步书中指出异步是一种并发编程模型。并发意味着不同的任务将交替执行其活动;例如,任务 A 执行一些工作,将线程交给任务 B,任务 B 执行一些工作后再将其交还,等等。
不要将其与并行编程混淆,并行编程是指同时运行多个任务的编程。你可以将并发和并行编程结合起来(例如,通过生成 Future),但我不会在这里讨论它,因为
async/.await
它用于实现并发编程,所以这里我主要关注并发编程。
简而言之,我们使用async
关键字告诉 Rust 一个块或一个函数将是异步的。
// asynchronous block
async {
// ...
}
// asynchronous function
async fn foo(){
// ...
}
但是, Rust 程序的异步性究竟意味着什么呢?它意味着它将返回该Future
特征的一个实现。我将Future
在下一节中详细介绍;目前,我们只需说 aFuture
代表一个可能已准备好也可能尚未准备好的值即可。
我们使用关键字来处理Future
异步块/函数返回的a .await
。考虑下面这个有点傻的例子:
async fn foo() -> i32 {
11
}
fn bar() {
let x = foo();
// it is possible to .await only inside async fn or block
async {
let y = foo().await;
};
}
在这种情况下,x
不是i32
,而是 trait 的实现Future
(在本例中)。另一方面,impl Future<Output = i32>
变量将是: 11。y
i32
另一种直观的理解方式是理解 Rust 会将其脱糖
async fn foo() -> i32 {}
变成这样
fn foo() -> impl Future<Output=i32>{}
当然,这里没有任何异步操作。但如果foo()
情况比较复杂,需要等待Mutex
锁或监听网络连接,Rust 不会一直占用线程,而是会尽可能多地处理后续工作,foo()
然后让线程去做其他事情,等到有更多工作可以做的时候再收回。
希望我们讲完Future
、Poll
和 等概念后,您能理解。现在,只要您对和Wake
的用法有个大概的了解就足够了。async
await
请务必阅读async/.await Primer。
期货
我认为说Future
trait 是异步 Rust 的心脏并不为过。
AFuture
是一种具有以下特征的特征:
- 一种
Output
类型(i32
在上面的例子中)。 - 一个
poll
函数。
poll()
是一个尽可能多地完成工作的函数,然后返回一个名为的枚举Poll
:
enum Poll<T> {
Ready(T),
Pending,
}
如你所见,我对.await
和 的描述poll()
有点重叠。这是因为调用.await
最终会调用poll()
。稍后会详细介绍。
这个枚举是我之前所写内容的表示,Future 表示一个可能准备好也可能还没准备好的值。
这个函数背后的基本思想很简单:当有人调用poll()
Future 时,如果它已经完全完成,它将返回,Ready(T)
并且.await
将返回T
。否则,它将返回Pending
。
问题是,如果它再次出现Pending
,我们该如何让它继续运转直至完成?答案很简单,就是反应堆。然而,在实现这一点之前,我们还有一些工作要做。
轮询、上下文、唤醒器、执行器和反应器
好多词啊!不过我真心觉得把所有东西都放在一起更容易理解,因为结合上下文更容易理解它们的作用。为了说明这一点,我提出了一个简化的假设场景。
假设我们有一个Future
通过async
关键字 created 的实例。让我们回顾一下 Future 是什么:
#[must_use = "futures do nothing unless you `.await` or poll them"]
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
我不会
Pin
在这里介绍,因为它有点复杂,而且没有必要理解这里发生的事情。
正如上面的代码所暗示的,Rust 中的未来是惰性的,这意味着仅仅声明它们不会使它们运行。
现在,假设我们使用 来运行 Future .await
。这里的“运行”意味着将其传递给将来会调用的“执行器” poll()
。
但是执行器是什么呢?简单来说,它是一种调度算法,实际上会轮询 Future。所以,当你调用 时.await
,执行器会负责执行工作。
好的,我们调用了.await
,future 被轮询并返回了Ready<T>
。发生了什么?.await
将会返回T
,执行器将清除该 Future,因此它不会再次被轮询。
或者,如果轮询的未来无法完成所有工作,它将返回Pending
。
收到 后Pending
,执行器将不会再次轮询 Future,除非它被告知。那么谁来通知它呢?“反应堆”。它将调用作为函数参数传递的wake()
上的函数。这使得执行器知道相关任务已准备好继续执行。Waker
poll()
但反应堆是什么?它是执行者的兄弟。当执行者在奥林匹斯山上管理一切,聆听
祈祷.await
反应堆位于冥府号上,与系统I/O协同工作,承担着繁重的工作。反应堆将知道poll
再次到达那个未来的最佳时机,并且它会发出指令wake()
。
那么,刚开始阅读 Rust 异步内容的你是否应该担心 executor 和 reactor 在后台是如何工作的呢?其实不必。为什么?因为当我们谈论 executor 和 reactor 时,我们已经在谈论运行时了;而当我们谈论运行时时,我们通常指的是Tokio。事实上,用executor和reactor这样的名字来调用它,就已经符合 Tokio 的命名法了。所以,最后,你所要做的就是将 Tokio 融入到你的项目中。通常的做法是在main
函数前使用它的过程宏:
#[tokio::main]
async fn main(){
// your async code
}
还是关于反应堆,Jon 花了 45 分钟在黑板上画图来解释,我可不敢说我能做得更好。所以,如果你想深入了解这个细节,可以看看上面的链接。
总结
让我们回顾一下:
async
用于创建异步块或函数,使其返回一个Future
。.await
将等待未来的完成并最终返回值(或错误,这就是为什么通常在中使用问号运算符的.await?
原因)。Future
是异步计算的表示,是可能已准备好或尚未准备好的值,由Poll
枚举的变体表示。Poll
是未来返回的枚举,其变体可以是Ready<T>
或Pending
。poll()
是执行 Future 并使其完成的函数。它接收一个Context
参数,并由执行器调用。Context
是 的包装器Waker
。Waker
是一种包含将被反应器wake()
调用的函数的类型,告诉执行器它可能会再次轮询未来。- Executor是一个通过重复调用来执行未来的调度程序
poll()
。 - Reactor有点像事件循环,负责唤醒待处理的未来。
好的,当然还有很多内容要谈,例如Send
和Sync
特征Pinning
等等,但我认为,对于初学者的帖子来说,我们已经说得足够多了。
下次再见!
封面艺术由TK创作。
文章来源:https://dev.to/rogertorres/asynchronous-rust-basic-concepts-44ed编辑 — 2021年9月1日:我做了一些修改,因为我意识到我为了简化某些内容而做出的努力,反而让它们听起来不对劲。这个问题可能还会在文中反复出现,所以如果你看到我为了简洁而牺牲了正确性,请指出来。