Rust 中的异步编程

了解 async-await 在 Rust 中如何工作。
2 位读者喜欢这篇文章。
Ferris the crab under the sea, unofficial logo for Rust programming language

Opensource.com

异步编程:非常有用,但很难学习。要创建一个快速且反应灵敏的应用程序,您无法避免异步编程。具有大量文件或网络 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 的一些神秘感和困惑,您必须了解 pinningboxing

如果您正在处理 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 中,单线程和多线程运行时都是可用的,因此即使在嵌入式环境中,您也可以从异步编程中受益。

标签
User profile image.
Stephan 是一位技术爱好者,他欣赏开源对事物如何运作的深刻洞察力。 Stephan 在工业自动化软件这个大多是专有领域的公司担任全职支持工程师。如果可能,他会从事他基于 Python 的开源项目,撰写文章或驾驶摩托车。

评论已关闭。

Creative Commons License本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
© . All rights reserved.