Rust异步编程

33-第七课-Rust异步编程/第七课 Rust异步编程.pdf

16-通过Futures库分析加深对Rust异步机制的理解/通过对futures库分析加深对 Rust 异步运行时的理解.pdf

08-利用 Tokio 实现一个高性能 Mini Http server-苏林/eight-利用Tokio实现一个高性能Mini-Http-Server.pdf


06-Tokio入门运行时介绍-苏林/six-Tokio入门运行时介绍.pdf


Rust中async/await 有什么区别?有什么作用?其他语言中有这两样东西吗


async/await 在Rust中主要用于异步编程,它们的区别和作用如下:

  • async - 用于声明一个异步函数,可以返回一个实现Future trait的类型。async函数会被编译器转换为状态机。

  • await - 用于在async函数中等待一个Future完成。await会使async函数暂停执行,直到等待的Future返回结果。

async/await的作用是让异步代码看起来更像是同步的,不需要显式地使用 Future 和库像tokio来驱动Future,使异步编程更简单。

其他语言中也有async/await:

  • JavaScript/Node.js - 异步编程的重要部分,与Rust类似。

  • C# - 从C# 7.0 开始引入,功能与Rust类似。

  • Python - 从Python 3.5开始引入asyncio模块和async/await,类似Rust。

  • Java - 从Java 8开始引入CompletableFuture,Java 15增加了预览版的async/await。

所以async/await目前在很多语言中都是实现异步编程的重要方式。Rust与其他语言的主要区别在于它利用异步函数的不确定性来保证内存安全。

05-Rust异步编程入门Future-苏林/Rust异步编程入门-Datafuse.pdf

在同步⽅法中调⽤阻塞函数(async转化的函数), 会阻塞整个线程, 但是, 阻塞的future会让出线程控制权, 允许其它future运⾏




async/await在不同语言中的实现有什么区别?


async/await在不同语言中的实现方式存在一些区别:

  • 在Rust中,async/await是利用生成器(generator)和Future实现的。编译器会将async函数转化为一个状态机,这使其零成本抽象。

  • 在JavaScript/Node.js中,async/await是建立在Promise之上的。async会隐式返回一个Promise,await就是等待一个Promise resolve。

  • C#中的async/await也是建立在Task(相当于Promise)之上的,原理与JavaScript类似。

  • Python中,async/await是基于协程coroutine实现的,编译器会将async函数转为协程对象。await会暂停协程并把控制权让回事件循环。

  • Java中的async/await是利用CompletableFuture实现的, await会提取CompletableFuture中的结果。

  • Go语言中没有async/await,但有类似的concept,通过channel和goroutine实现类似的异步编程方式。

总结一下区别:

  • Rust和Python通过生成器/协程实现抽象,性能高效但需要编译。

  • JavaScript/Node.js/C#通过Promise/Task实现,依赖运行时支持。

  • Java通过Future实现,需要手动编码异步逻辑。

  • Go语言通过自带的 csp 模型实现。

所以async/await的实现方式跟语言的运行时和并发模型有很大关系。



在Rust中,async/await是如何保证内存安全的?


在Rust中,async/await之所以可以保证内存安全,主要依赖于三点:

  1. 所有权系统(Ownership)

Rust的所有权系统确保每个值都有一个唯一的所有者。在async函数中,所有权规则同样适用。这确保了在异步代码中,内存资源可以被正确地清理和回收。

  1. 借用检查器(Borrow checker)

Rust的借用检查器可以在编译时检测异步代码中的错误。确保引用在异步代码中不会失效。

  1. Future trait

异步函数被编译成实现Future trait的状态机。Future对异步操作进行抽象,以一个统一的接口表示异步计算的状态。这使Rust可以知道异步操作的状态变化,来进行正确的内存管理。

  1. Pinning

Rust中通过Pin来固定Future,确保当异步函数暂停或者恢复时,其内存不会被移动或销毁。这也是保证内存安全的重要方式。

5.栈无关编译(Stackless compilation)

Rust编译器会将async函数转换成状态机,而不再依赖调用栈。这使其可以长时间暂停而不占用栈内存。

综合这几点,Rust的语言特性和编译器的处理,让其异步编程可以在编译期就保证内存安全,这是一个独特的优势。



除了async/await,Rust还有其他用于异步编程的工具吗?


是的,除了async/await,Rust还提供了一些其他工具用于异步编程:

  • Future 和 Stream traits:这两个traits在异步编程中非常重要,Future 表示一个异步的 calculation,Stream 表示一个异步的值的序列。许多异步函数会产生这两种 traits。

  • Tokio:这是一个事件驱动的异步运行时,提供了基于 Future/Stream 的异步任务调度和执行。Tokio 中有一些重要的组件,比如Tokio runtime。大多数Rust异步代码会依赖 Tokio。

  • Async std:这是 std 库的异步版本,提供了文件、网络等操作的异步接口,基于 Future 设计。可以看作是异步版的 std 库。

  • Async channels:Rust提供了一些异步的channels,例如 async-channel, message-passing 的异步实现。

  • Async trait:一个 proc macro 库,可以自动生成 traits 的异步版本。

  • Generators:Rust的生成器(generator)功能可以用来实现异步流处理。

  • Pinning:Pinning 可以确保 Future 在内存中的位置不变,这对异步编程非常重要。

  • Waker:用来在 Future 完成时发送通知的对象。

所以除了 async/await 以外,Rust还提供了许多其他原语和抽象来支持异步编程,这都建立在所有权、借用检查等语言特性之上。



有没有一些示例代码可以帮助我更好地理解Rust中的异步编程?


这里提供一些Rust异步编程的示例代码,可以帮助你更好地了解它:

使用tokio运行异步任务

1
2
3
4
5
6
7
8
9
10
11
12
use tokio::time::timeout;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
timeout(Duration::from_secs(5), async {
println!("Doing long work...");
tokio::time::sleep(Duration::from_secs(3)).await;
println!("Done!");
}).await?;
Ok(())
}

使用tokio和async/await处理TCP连接

1
2
3
4
5
6
7
8
9
use tokio::net::TcpStream;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let stream = TcpStream::connect("127.0.0.1:8080").await?;

// 处理TCP连接
Ok(())
}

使用 futures::future 建立Future类型的异步任务

1
2
3
4
5
6
7
8
9
10
use futures::future;

fn long_task() -> impl Future<Output=()> {
future::ready(())
}

#[tokio::main]
async fn main() {
tokio::spawn(long_task());
}

这些示例展示了常见的 Rust 异步编程模式,比如使用 tokio 运行异步任务、处理 TCP 连接、构建 Future 等。它们能帮助你掌握 Rust 异步编程的基本概念和用法。


在Rust中,async/await是如何转换成状态机的?


在Rust中,async函数会被转换成状态机来实现异步操作。

主要过程如下:

  1. 当调用async函数时,它不会立即执行,而是返回一个Future对象。

  2. Future对象持有该异步任务的执行状态和环境信息。

  3. 当需要执行异步任务时(比如使用await或调用poll方法),会将Future提交到事件循环执行poll方法。

  4. poll方法会检查任务的当前状态,如果需要等待事件就返回Pending状态。如果任务已完成就返回Ready状态并返回结果。

  5. 任务的各个await点被抽取出来封装成结构体(State),并将async函数主体代码封装成Generator/闭包。

  6. 每次poll都会进行状态迁移,执行下一个await块的代码,直到全部执行完成。

例如:

1
2
3
4
async fn foo() {
await1();
await2();
}

会被转换为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct FooState {
state: State,
}

impl Future for FooState {
fn poll(&mut self) -> Poll<()> {
match self.state {
State1 => {
// 执行await1代码
self.state = State2;
}
State2 => {
// 执行await2代码
return Poll::Ready(());
}
}
}
}

这就是Rust异步编程底层是如何实现的状态机模式。



async/await在Rust中的内存管理是如何工作的?


Rust中async/await的内存管理主要有以下几点:

  1. async函数会返回一个Future对象。Future对象实现了’Pin’和’Unpin’ trait,它会决定何时移动值或借用值。

  2. 当async函数返回Future对象时,它不会立即执行,所以此时不需要引用任何地方的数据。

  3. Future对象在执行poll方法时,如果需要使用async函数闭包里的变量,会进行“pinning”操作将其固定在内存里。

  4. 并发执行多个Future时,调度器会周期性地执行每个Future的poll方法。这里不会同时运行多个Future闭包里的代码,所以不会出现数据竞争。

  5. Future对象会跟踪async函数打开的所有作用域。每次poll只会运行特定作用域内的代码,其他作用域的数据不会被访问到。

  6. async闭包里声明的变量的生命周期与Future对象绑定。只有Future还在栈上时,这些变量才能访问到。

  7. Future object在完成或被丢弃后,会自动清理async函数打开的所有作用域。

所以可以总结为:通过Pinning和定期轮叫poll方法,Rust能够解决async函数中变量的内存管理问题,并发访问不会产生数据竞争。Future的生命周期管理也能保证变量不会被提前释放。


Rust中的Pinning是如何确保内存安全的?


Rust中的Pinning机制是一种保证内存安全的重要机制。它主要做到了以下几点:

  • 通过在结构体上实现Pin trait,可以显式声明该结构体可能会被“pinned”,防止被移动。

  • 当将某个值pinned到内存中的特定地址时,Rust编译器会生成代码来阻止该值被移动到其他地址。

  • 每次poll操作时,Future的实现会确认所引用的值不会在 poll 期间被移动或发生构造/解构行为。

  • async fn 返回的 Future 对象默认为Unpin,需要显示调用pin_mut来获取可变引用以修改内部状态。

  • pin_mut后 Future 无法再被移动,而内部值也不会被移动,这保证了访问内部值的安全性。

  • Rust会禁止在pinned对象上调用会导致其移动的方法,如drop。

  • Future在完成后会自动unpin内部值,解除对它们的限制。

通过这些保证,Rust可以在允许并发和异步操作的同时,防止值在poll调用期间被意外移动或释放,从而避免使用已释放内存的 bug。

所以Pinning可以看作是Rust异步安全设计的一个关键支柱。它通过限制值的移动行为,保障了异步上下文中的内存安全性。