异步编程:非常有用,但很难学习。要创建一个快速且反应灵敏的应用程序,您无法避免异步编程。具有大量文件或网络 I/O 或具有始终应该是反应灵敏的 GUI 的应用程序会从异步编程中受益匪浅。任务可以在后台执行,而用户仍然可以进行输入。异步编程在多种语言中都是可能的,每种语言都有不同的风格和语法。Rust 也不例外。在 Rust 中,此功能称为 async-await。
虽然 async-await 自 1.39.0 版本以来一直是 Rust 的组成部分,但大多数应用程序都依赖于社区 crates。在 Rust 中,除了更大的二进制文件外,async-await 没有成本。本文让您深入了解 Rust 中的异步编程。
底层原理
要基本了解 Rust 中的 async-await,您实际上是从中间开始。
async-await 的中心是 future trait,它声明了方法 poll(我将在下面更详细地介绍)。如果可以异步计算一个值,则相关类型应实现 future trait。重复调用 poll 方法,直到最终值可用。
此时,您可以从您的同步应用程序手动重复调用 poll 方法,以获得最终值。但是,由于我正在谈论异步编程,您可以将此任务交给另一个组件:运行时。因此,在您可以使用 async 语法之前,必须存在运行时。在下面的示例中,我使用了来自 tokio 社区 crate 的运行时。
使 tokio 运行时可用的一种便捷方法是在您的 main 函数上使用 #[tokio::main]
宏
#[tokio::main]
async fn main(){
println!("Start!");
sleep(Duration::from_secs(1)).await;
println!("End after 1 second");
}
当运行时可用时,您现在可以 await futures。Awaiting 意味着只要 future 需要完成,进一步的执行就会在此处停止。await 方法会导致运行时调用 poll 方法,这将驱动 future 完成。
在上面的例子中,tokios 的 sleep 函数返回一个 future,该 future 在指定的持续时间过去后完成。通过等待这个 future,相关的 poll 方法会被重复调用,直到 future 完成。此外,由于 fn 之前的 async
关键字,main() 函数也返回一个 future。
所以如果你看到一个标记为 async
的函数:
async fn foo() -> usize { /**/ }
那么它只是以下语法糖
fn foo() -> impl Future<Output = usize> { async { /**/ } }
Pinning 和 boxing
要消除 Rust 中 async-await 的一些神秘感和困惑,您必须了解 pinning 和 boxing。
如果您正在处理 async-await,您将相对较快地遇到 boxing 和 pinning 这两个术语。由于我发现关于这个主题的可用解释很难理解,所以我给自己设定了更容易解释这个问题的目标。
有时需要拥有保证在内存中不会移动的对象。当您有一个自引用类型时,这会生效
struct MustBePinned {
a: int16,
b: &int16
}
如果成员 b 是对同一实例的成员 a 的引用(指针),那么当实例被移动时,引用 b 将变为无效,因为成员 a 的位置已更改,但 b 仍然指向先前的位置。您可以在 Rust Async book 中找到一个更全面的 自引用 类型的示例。您现在需要知道的是,MustBePinned 的实例不应在内存中移动。像 MustBePinned 这样的类型没有实现 Unpin trait,这将允许它们在内存中安全地移动。换句话说,MustBePinned 是 !Unpin。
回到未来:默认情况下,future 也是 !Unpin;因此,它不应该在内存中移动。那么你如何处理这些类型?您 pin 和 box 它们。
Pin<T> 类型包装指针类型,保证指针后面的值不会被移动。Pin<T> 类型通过不提供包装类型的可变引用来确保这一点。该类型将在对象的生命周期内被固定。如果您不小心固定了一个实现 Unpin (可以安全移动) 的类型,它不会有任何影响。
在实践中:如果您想从函数返回一个 future (!Unpin),您必须 box 它。使用 Box<T> 会导致该类型被分配到堆而不是栈上,从而确保它可以超过当前函数的生命周期而不会被移动。特别是,如果您想传递一个 future,您只能传递一个指向它的指针,因为 future 必须是类型 Pin<Box<dyn Future>>。
使用 async-wait,您肯定会遇到这种 boxing 和 pinning 语法。要总结这个主题,您只需要记住这个
- Rust 不知道一个类型是否可以安全移动。
- 不应移动的类型必须包装在 Pin<T> 中。
- 大多数类型都是 Unpinned 类型。它们实现了 trait Unpin,可以在内存中自由移动。
- 如果一个类型被包装在 Pin<T> 中,并且包装的类型是 !Unpin,则不可能从中获取可变引用。
- 由 async 关键字创建的 Futures 是 !Unpin,因此必须被 pinned。
Future trait
在 future trait 中,一切都汇集在一起
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
这是一个如何实现 future trait 的简单示例
struct MyCounterFuture {
cnt : u32,
cnt_final : u32
}
impl MyCounterFuture {
pub fn new(final_value : u32) -> Self {
Self {
cnt : 0,
cnt_final : final_value
}
}
}
impl Future for MyCounterFuture {
type Output = u32;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32>{
self.cnt += 1;
if self.cnt >= self.cnt_final {
println!("Counting finished");
return Poll::Ready(self.cnt_final);
}
cx.waker().wake_by_ref();
Poll::Pending
}
}
#[tokio::main]
async fn main(){
let my_counter = MyCounterFuture::new(42);
let final_value = my_counter.await;
println!("Final value: {}", final_value);
}
这是一个如何手动实现 future trait 的简单示例:future 使用一个它应该计数到的值进行初始化,存储在 cnt_final 中。每次调用 poll 方法时,内部值 cnt 都会递增 1。如果 cnt 小于 cnt_final,则 future 向运行时的 waker 发出信号,表示 future 已准备好再次被轮询。Poll::Pending
的返回值表示 future 尚未完成。在 cnt >= cnt_final 之后,poll 函数返回 Poll::Ready
,表示 future 已完成并提供最终值。
这只是一个简单的例子,当然,还有其他需要注意的事情。如果您考虑创建自己的 futures,我强烈建议您阅读 tokio crate 文档中的 Async in depth 章节。
总结
在我总结之前,这里有一些我认为有用的附加信息
- 使用 Box::pin 创建一个新的 pinned 和 boxed 类型。
- futures crate 提供了类型 BoxFuture,它允许您将 future 定义为函数的返回类型。
- async_trait 允许您在 traits 中定义一个 async 函数(目前不允许)。
- pin-utils crate 提供了用于 pin 值的宏。
- tokios 的 try_join! 宏 (a)waits 多个返回 Result<T, E> 的 futures。
一旦克服了最初的障碍,Rust 中的异步编程就很简单了。如果您可以将可以在异步函数中并行执行的代码外包,您甚至不必在自己的类型中实现 future trait。在 Rust 中,单线程和多线程运行时都是可用的,因此即使在嵌入式环境中,您也可以从异步编程中受益。
评论已关闭。