【跟小嘉学 Rust 编程】二十七、Rust 异步编程(Asynchronous Programming)

系列文章目录

【跟小嘉学 Rust 编程】一、Rust 编程基础
【跟小嘉学 Rust 编程】二、Rust 包管理工具使用
【跟小嘉学 Rust 编程】三、Rust 的基本程序概念
【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念
【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据
【跟小嘉学 Rust 编程】六、枚举和模式匹配
【跟小嘉学 Rust 编程】七、使用包(Packages)、单元包(Crates)和模块(Module)来管理项目
【跟小嘉学 Rust 编程】八、常见的集合
【跟小嘉学 Rust 编程】九、错误处理(Error Handling)
【跟小嘉学 Rust 编程】十一、编写自动化测试
【跟小嘉学 Rust 编程】十二、构建一个命令行程序
【跟小嘉学 Rust 编程】十三、函数式语言特性:迭代器和闭包
【跟小嘉学 Rust 编程】十四、关于 Cargo 和 Crates.io
【跟小嘉学 Rust 编程】十五、智能指针(Smart Point)
【跟小嘉学 Rust 编程】十六、无畏并发(Fearless Concurrency)
【跟小嘉学 Rust 编程】十七、面向对象语言特性
【跟小嘉学 Rust 编程】十八、模式匹配(Patterns and Matching)
【跟小嘉学 Rust 编程】十九、高级特性
【跟小嘉学 Rust 编程】二十、进阶扩展
【跟小嘉学 Rust 编程】二十一、网络编程
【跟小嘉学 Rust 编程】二十三、Cargo 使用指南
【跟小嘉学 Rust 编程】二十四、内联汇编(inline assembly)
【跟小嘉学 Rust 编程】二十五、Rust命令行参数解析库(clap)
【跟小嘉学 Rust 编程】二十六、Rust的序列化解决方案(Serde)
【跟小嘉学 Rust 编程】二十七、Rust 异步编程(Asynchronous Programming)

文章目录

  • 系列文章目录
    • @[TOC](文章目录)
  • 前言
  • 一、并发的概念
    • 1.1、什么是并发(concurrency)
    • 1.2、什么是线程
    • 1.3、并发编程的难点
    • 1.4、死锁
    • 1.5、线程
      • 1.5.1、线程
      • 1.5.2、线程句柄(JoinHandler)
      • 1.5.3、park 和 unpark
      • 1.5.4、共享线程状态
      • 1.5.5、互斥锁
        • 1.5.5.1、互斥锁
        • 1.5.5.2、互斥锁的中毒状态
    • 1.6、通道(channel)
      • 1.6.1、通道(channel)
      • 1.6.2、std::sync::mpsc
  • 二、I/O
    • 2.1、输入输出 特征
      • 2.1.1、输入输出 特征
      • 2.1.2、读取方法
      • 2.1.3、写入方法
    • 2.2、BufReader
      • 2.2.1、BufReader
      • 2.2.2、BufRead trait
    • 2.3、BufWriter
    • 2.4、StdinLock
  • 三、异步编程
    • 2.1、异步编程
      • 2.1.1、程序任务两种类型
      • 2.1.2、同步实现和多线程实现
    • 2.2、异步编程
      • 2.2.1、为什么要用异步
      • 2.2.2、其他四种编程模型
      • 2.2.3、Rust 中异步支持
      • 2.2.4、Rust的异步 VS 线程
        • 2.2.4.1、线程模型(thread)
        • 2.2.4.2、异步(async)
      • 2.2.5、Rust 异步的当前进展
        • 2.2.5.1、Rust 异步的当前进展
        • 2.2.5.2、Rust 语言和库的支持
    • 2.3、async/await 入门
      • 2.3.1、async
      • 2.3.2、await
      • 2.3.3、async 的生命周期
      • 2.3.3、async move
    • 2.4、理解Future 和任务调度
      • 2.4.1、Future trait
      • 2.4.2、使用 Waker 唤醒任务
      • 2.4.3、执行器 Executor
      • 2.4.4、执行者和系统I/O
    • 2.5、async 和 .await
      • 2.5.1、什么是async 和 .await
      • 2.5.2、使用 async 的三种方式
      • 2.5.3、async 生命周期
      • 2.5.4、async move
    • 2.6、Pin 和 Unpin
      • 2.6.1、Pin
      • 2.6.2、Unpin
      • 2.6.3、Pin 在实践中的运用
        • 2.6.3.1、将值固定到栈上
        • 2.6.3.2、固定到堆上
        • 2.6.3.3、将固定住的 Future 变为 Unpin
    • 2.7、Stream
      • 2.7.1、Stream
      • 2.7.2、迭代和并发
    • 2.8、join!和 select!
      • 2.8.1、join!
      • 2.8.2、 try_join!
      • 2.8.3、 select!
        • 2.8.3.1、default => ... 和 complete =>...
        • 2.8.3.2、与 Unpin 和 FusedFuture 交互
        • 2.8.3.3、在 select 循环并发
    • 2.9、async 语句中使用?
    • 2.10、async 函数 和 Send 特征
    • 2.11、递归使用 async fn
    • 2.12、在特种中使用 async

前言

本章节讲解 Rust 的异步编程方案,我们将讲解 Future、Waker、Executor、Pin、async 和 await、Stream等

主要教材参考 《The Rust Programming Language》
主要教材参考 《Rust For Rustaceans》
主要教材参考 《The Rustonomicon》
主要教材参考 《Rust 高级编程》
主要教材参考 《Cargo 指南》
主要教材参考 《Rust 异步编程》


一、并发的概念

1.1、什么是并发(concurrency)

  • 并发是程序同时有多个正在运行的线程。
  • 线程之间可以共享数据,而不引入通信(如网络、进程间通信)的开销。
  • 线程比单独的进程要轻量级: 在线程之间切换的时候不会发生大开销的操作系统上下文切换动作;

1.2、什么是线程

  • 线程是指令执行的上下文,以及对一些数据的引用关系(可能是共享的)。
  • 上下文包括一组寄存器的值、一个栈,以及其他与当前执行上下文相关的信息。
  • 每个程序至少有一个线程。
  • 有一个线程调度器 (scheduler) 来管理线程的执行,决定什么时候运行哪个线程。
  • 程序可以创建新的线程,由调度器负责运行。

1.3、并发编程的难点

  • 数据共享:两个线程同时试图修改同一份数据;
  • 数据竞争:同一段代码的行为与它的执行情况有关;
  • 同步:如何保证所有线程都有正确的世界观,在线程间发送数据,如何确保其他线程在正确的地方收到了数据
  • 死锁:如果线程间安全地共享资源,确保线程不会相互锁定数据访问行为

1.4、死锁

当多个线程访问某一共享资源时,可能会发生死锁。死锁发生后,所有参与的线程都无法访问数据。

死锁发生有四个条件

  • 互斥:资源以非共享的模式锁定。
  • 持有资源:线程持有一个资源,并去要求获得其他线程持有的资源。
  • 非抢占:持有资源的线程不会自愿释放资源。
  • 等待成环:等待资源的线程之间形成环状关系。

1.5、线程

1.5.1、线程

Rust 的标准库提供了 线程 std::thread ,每个线程都有自己的栈和状态,使用闭包指定线程的行为


use std::thread;

fn main() {
    thread::spawn(||{
        println!("Hello, World!");
    });
}

1.5.2、线程句柄(JoinHandler)

thread::spawn 返回 JoinHandler 类型的线程句柄 (handlers)。


use std::thread;

fn main() {
    let handler = thread::spawn(||{
        println!("Hello, World!");
    });

    println!("{:?}", handler.join());
}
  • join() 会阻塞当前线程的执行,直到句柄对应的线程终止。
  • join() 返回对应线程返回值的 Ok,或者是 Err。

当线程的句柄杯丢弃的时候,线程会变为失联(detached)状态。此时它还在运行,不能克隆线程句柄,只有一个变量有权限来做线程的汇合(join)。

如果线程发生 panic,那么将无法从该线程内部进行恢复。

  • Rust 的线程如果发生恐慌,和创建这个线程的线程没有关系。

    • 只有发生恐慌的线程会崩溃。
    • 这个线程会进行相关的清理工作,包括栈和其他资源。
    • 其他线程能够读取恐慌的消息。
  • 如果主线程恐慌或者正常结束,其他线程也会被终止。

    • 主线程可以在自己结束之前去等待其他线程结束。

1.5.3、park 和 unpark

  • 当前正在运行的线程可以调用 thread::park() 来暂停自己的执行;
  • 可以通过线程的unpark() 来继续执行;

use std::{thread, time::Duration};

fn main() {
    let handler = thread::spawn(||{
        thread::park();
        println!("Hello, World!");
    });

    println!("main");

    thread::sleep(Duration::from_micros(100));
    handler.thread().unpark();
}
  • 使用 JoinHandler 提供了 thread() 方法来获取对应线程的 Thread 对象
  • 当前正在运行的线程 通过 thread:: current() 获得

1.5.4、共享线程状态

Rust 类型系统包含了满足并发承诺的特征

  • Send:可以在线程间安全转移的类型
  • Sync:可以在线程间(通过引用)安全共享的类型

1.5.5、互斥锁

1.5.5.1、互斥锁

  • 互斥锁 (mutexes) 是 Mutual Exclusion 的缩写
  • 互斥锁保证它所包含的值在同一时刻只有一个线程能够访问。
  • 为了访问由互斥锁保护的数据,需要获得互斥锁中的锁。
  • 如果有人正拿着锁,那么其他人可以选择放弃并在后面再尝试,或者阻塞等待直到锁被释放
  • 用 Mutex 包裹一个值时,需要调用 lock 方法来获取对值的访问权限,lock 方法返回一个 LockResult
  • 如果互斥锁是锁定的状态,lock 方法会阻塞等待,直到锁被释放
    • 也可以使用 try_lock 方法避免阻塞等待。锁定成功后可以得到一个MutexGuard,通过解引用来访问里面的 T 类型数据。

1.5.5.2、互斥锁的中毒状态

如果一个线程锁定了一个互斥锁,然后发生了 panic ,此时互斥锁会进入中毒(poisoned) 状态,因为这个锁不会被释放了。

ock 方法返回一个 LockResult。

  • 如果是 Ok(MutexGuard),那么互斥锁没有中毒,可以正常使用。
  • 如果是 Err(PoisonError),那么互斥锁处于中毒状态

1.6、通道(channel)

1.6.1、通道(channel)

通道(channel)可以用来同步线程之间的状态,通道能够在线程之间传递数据,可以用来提醒其他线程关于数据已经就绪、事件已经发生等情况。

1.6.2、std::sync::mpsc

实现多生产者、单消费者的通信功能(Muliti-Producer, Single-Consumer)。

主要设计三种类型

  • Sender :用于给 Receiver 发送数据,可以克隆,交给多个线程实现多生产者

  • SyncSender:用于给 Receiver 发送数据

  • Receiver:不能克隆,因此是单消费者

  • 使用 channel 函数创建一对链接,Sender 是异步通道,发送数据的时候不会阻塞发生线程。

  • 使用 sync_channel 函数可以创建同步通道,发送消息的时候会阻塞;

二、I/O

2.1、输入输出 特征

2.1.1、输入输出 特征

pub trait Read {
	fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
	// Other methods implemented in terms of read().
}

pub trait Write {
	fn write(&mut self, buf: &[u8]) -> Result<usize>;
	fn flush(&mut self) -> Result<()>;
	// Other methods implemented in terms of write() and flush().
}
  • 很多类型都实现了 标准的 IO特征:例如 File、TcpStream、Vec、等
  • 反回类型是 std::io::Result,而不是 std:: Result;

2.1.2、读取方法

/// Required.
fn read(&mut self, buf: &mut [u8]) -> Result<usize>;

/// Reads to end of the Read object.
fn read_to_end(&mut self, buf: &mut Vec<u8>) -> Result<usize>;

/// Reads to end of the Read object into a String.
fn read_to_string(&mut self, buf: &mut String) -> Result<usize>;

/// Reads exactly the length of the buffer, or throws an error.
fn read_exact(&mut self, buf: &mut [u8]) -> Result<()>;

// 读取迭代器
fn bytes(self) -> Bytes<Self> where Self: Sized

// 迭代器适配器
// chain 以第二个读取对象作为输入,返回的迭代器先迭代 self,再迭代 next。
fn chain<R: Read>(self, next: R) -> Chain<Self, R>
where Self: Sized

// take 创建的迭代器的迭代范围是读取对象的前 limit 个字节。
fn take<R: Read>(self, limit: u64) -> Take<Self>
where Self: Sized
  • bytes 方法将 Read 按逐字节的方式转换成迭代器
  • 关联的 Item 是 Result
    • 调用 next() 返回的是 Option>
    • 遇到 EOF 时会返回 None。

2.1.3、写入方法

pub trait Write {
	fn write(&mut self, buf: &[u8]) -> Result<usize>;
	fn flush(&mut self) -> Result<()>;

	/// Attempts to write entire buffer into self.
	fn write_all(&mut self, buf: &[u8]) -> Result<()> { ... }
	
	/// Writes a formatted string into self.
	/// Don't call this directly, use `write!` instead.
	fn write_fmt(&mut self, fmt: Arguments) -> Result<()> { ... }
}

2.2、BufReader

2.2.1、BufReader

BufReader 可以给任意的读取对象添加缓冲机制,实现了 Read trait,因此可以透明的使用

2.2.2、BufRead trait

BufReader 还实现了 BufRead 特征

pub trait BufRead: Read {
	fn fill_buf(&mut self) -> Result<&[u8]>;
	fn consume(&mut self, amt: usize);
	
	// 读取方法
	fn read_until(&mut self, byte: u8, buf: &mut Vec<u8>) -> Result<usize>
	fn read_line(&mut self, buf: &mut String) -> Result<usize> 
	
	// 迭代器
	fn split(self, byte: u8)-> Split<Self> where Self: Sized 
    fn lines(self)-> Lines<Self> where Self: Sized 
}

2.3、BufWriter

BufWriter 实现了 Write 的trait,直接缓存所有写入的数据,在失效前会把所有缓存的数据写出。

BufWriter 没有像 BufReader 那样实现新的特型,他直接缓存所有的写入的数据,在失效前把所有缓存的数据写出。

2.4、StdinLock

  • 多个线程从标准输入读取数据是一个数据共享问题,需要加锁。
  • 所有的 read 方法会在内部调用 self.lock()。
  • 也可以创建 StdinLock 并显式调用 lock() 方法。
let lock: io::StdinLock = io::stdin().lock();

三、异步编程

2.1、异步编程

2.1.1、程序任务两种类型

  • CPU-bound:CPU密集型任务,实现文件压缩、视频编码、图形处理等,可以利用多核(多cpu、多机)来获得整体性能
  • IO-bound:IO密集型任务,文件读写、网络请求处理等,希望等IO完成的过程中可以利用 CPU 做其他任务

2.1.2、同步实现和多线程实现

  • 同步实现缺点:同一时刻只能处理一个请求
  • 多线程实现:针对每个请求,开启一个新的线程来处理
    • 优点:提高性能,可同时处理多个请求
    • 缺点:引入各种开销和并发的问题

2.2、异步编程

2.2.1、为什么要用异步

异步编程是一种并发编程模型,在很多语言里都支持。允许使用少量的操作想听线程来处理一堆并发任务

2.2.2、其他四种编程模型

  • OS Thread:不需要改动编程模型,这使得并发变得简单很多。但是多线程异步编程则变得困难许多,同时性能开销上也会变大,毕竟需要开很多线程。我们之前学的线程池可以缓和部分性能开销,但是对于大规模密集型工作来说还是不够的。

  • 事件驱动编程:搭配callbacks也就是回调,性价比高很多,但是代价则是冗长繁杂的写法、非线性(non-linear)工作流(比如前端的回调地狱)。以及很难跟踪数据和错误产生的地点。

  • Coroutines:和多线程类似,并不需要改动编程模型,这也就意味着用起来比较简单。同时它也和异步类似,可以支持大量的任务。而代价则是高度抽象,一些重要的底层细节都不会暴露出来。这也就意味着你想去自定义一些较底层的特性是很难实现的。

  • Actor模型: actors指的是将所有的并发计算切割成一小个一小个的单元(unit),这些个单元通过易错信息(fallible message)来沟通,比较像分布式系统(distributed systems)。它可以有效的实施,但是它还有一些实用性理论比如控制流(control flow)和重试逻辑(retry logic)还没完善。

2.2.3、Rust 中异步支持

  • Future:是惰性的,只有 poll 时才能取得进展,被丢弃的 future 就无法取得进展;
  • Async:是零成本的,使用 async,可以无需堆内存分配(heap allocation) 和动态调度(dynamic dispatch),对性能大好,且允许在受限环境使用 async;
  • 不提供内置运行时,运行时由社区提供
  • 单线程、多线程均支持,但是优缺点不同

2.2.4、Rust的异步 VS 线程

注意:异步 并不一定比线程好;

2.2.4.1、线程模型(thread)

在 Rust 中如果不想使用异步,那么一般替代方案就是 OS Thread了。

如果你项目并发量不大,也就是任务比较少,那么首选 OS Thread了。这些线程都是来自 CPU 的,所有会有内存开销。创建/切换线程是非常昂贵的行为,即使是空闲线程也会吃资源

这对于驱动器(drivers)和其他延迟灵敏的应用(latency sensitive)的应用

2.2.4.2、异步(async)

异步则可以明显的减少对于CPU和内存的开销,尤其是在大规模密集型任务中,比如服务器、数据库。

代价就是大量的 binary blob(二进制斑点),他们来自 async function 生成的状态机(state matchines)。因为每个可执行文件都被捆绑到一个异步 runtime 上了。

2.2.5、Rust 异步的当前进展

2.2.5.1、Rust 异步的当前进展

Rust 异步特性目前大部分稳定,另一些特性还在完善之中。

特点

  • 针对典型并发任务,性能出色
  • 与高级语言特性频繁交互(生命周期、pining)
  • 同步和异步代码间、不同运行时的异步代码存在兼容性约束
  • 由于不断进步,维护负担更重

2.2.5.2、Rust 语言和库的支持

Rust 本身支持异步编程,但是大多数异步应用程序依赖于社区提供的功能。

  • 标准库提供了最基本的特性、类型和功能,例如 Future trait
  • async/await 语法直接被 Rust 编译器支持
  • future crate 提供了许多实用类型函数、宏和函数,他们可以用于任何异步应用程序
  • 异步代码、IO和任务生成的执行有 async runtimes 提供,例如 tokio、async-std、futures、smol、fuchsia-async;
  • 没有对异步任务进行调度执行的支持
  • Rust 将异步运行时交给第三方库来提供

2.3、async/await 入门

2.3.1、async

  • async 把一段代码块转化为一个实现了 Future 特征(trait) 的 状态机(关于状态机,我们会在别的章节去讲解)
  • 虽然在同步方法中调用阻塞函数会阻塞整个线程,但阻塞的 Future 将放弃对线程的控制,从而允许其他 Future 来运行

添加依赖

 cargo add futures

使用

use futures::executor::block_on;

async fn do_something() {
    println!("hello,world");
}

fn main() {
    let future = do_something();
    block_on(future); //需要使用执行器执行
}

Async 函数返回值是一个 Future ,Future 要传给 block_on 或者其他的执行者执行。

2.3.2、await

在 async 函数中,可以使用 .await 来等待另一个 Future 特征(trait) 的完成,与 block_on 不同,.await 不会阻塞当前线程,而是异步的等待 Future完成。

use futures::executor;

async fn learn_song(){
    println!("learn songe");
}

async fn sing_song(){
    println!("learn songe");
}

async fn dance(){
    println!("dance");
}

async fn learn_and_sing_sone(){
   learn_song().await; // 阻塞执行等待执行完成
   sing_song().await;
}

async fn  async_main(){
    let f1= learn_and_sing_sone();
    let f2 = dance();
    futures::join!(f1, f2); // f1 f2 并发执行
}

fn main() {
    executor::block_on(async_main());
    println!("main end");
}

2.3.3、async 的生命周期

async fn 函数如果拥有引用类型的参数,那它返回的 Future 的生命周期就会被这些参数的生命周期所限制:

async fn foo(x: &u8) -> u8 { *x }

// 上面的函数跟下面的函数是等价的:
fn foo_expanded<'a>(x: &'a u8) -> impl Future<Output = u8> + 'a {
    async move { *x }
}

意味着 async fn 函数返回的 Future 必须满足以下条件: 当 x 依然有效时, 该 Future 就必须继续等待( .await ), 也就是说 x 必须比 Future 活得更久。

在一般情况下,在函数调用后就立即 .await 不会存在任何问题,例如foo(&x).await。但是,若 Future 被先存起来或发送到另一个任务或者线程,就可能存在问题了:

use std::future::Future;
fn bad() -> impl Future<Output = u8> {
    let x = 5;
    borrow_x(&x) // ERROR: `x` does not live long enough
}

async fn borrow_x(x: &u8) -> u8 { *x }

上述代码就存在编译错误,因为 x 的生命周期只到 bad 函数,但是 Future 显然会活得更久。

error[E0597]: `x` does not live long enough
 --> src/main.rs:4:14
  |
4 |     borrow_x(&x) // ERROR: `x` does not live long enough
  |     ---------^^-
  |     |        |
  |     |        borrowed value does not live long enough
  |     argument requires that `x` is borrowed for `'static`
5 | }
  | - `x` dropped here while still borrowed

其中一个常用的解决方法就是将具有引用参数的 async fn 函数转变成一个具有 'static 生命周期的 Future 。 以上解决方法可以通过将参数和对 async fn 的调用放在同一个 async 语句块来实现:

use std::future::Future;

async fn borrow_x(x: &u8) -> u8 { *x }

fn good() -> impl Future<Output = u8> {
    async {
        let x = 5;
        borrow_x(&x).await
    }
}

如上所示,通过将参数移动到 async 语句块内, 我们将它的生命周期扩展到 'static, 并跟返回的 Future 保持了一致。

2.3.3、async move

async 允许我们使用 move 关键字来将环境中变量的所有权转移到语句块内,就像闭包那样,好处是你不再发愁该如何解决借用生命周期的问题,坏处就是无法跟其它代码实现对变量的共享:

// 多个不同的 `async` 语句块可以访问同一个本地变量,只要它们在该变量的作用域内执行
async fn blocks() {
    let my_string = "foo".to_string();

    let future_one = async {
        // ...
        println!("{my_string}");
    };

    let future_two = async {
        // ...
        println!("{my_string}");
    };

    // 运行两个 Future 直到完成
    let ((), ()) = futures::join!(future_one, future_two);
}

// 由于 `async move` 会捕获环境中的变量,因此只有一个 `async move` 语句块可以访问该变量,
// 但是它也有非常明显的好处: 变量可以转移到返回的 Future 中,不再受借用生命周期的限制
fn move_block() -> impl Future<Output = ()> {
    let my_string = "foo".to_string();
    async move {
        // ...
        println!("{my_string}");
    }
}

2.4、理解Future 和任务调度

2.4.1、Future trait

Future trait 是 Rust 异步编程的核心,毕竟异步函数是异步编程的核心,而 Future 恰恰是异步函数的返回值和被执行的关键。

trait SimpleFuture {
    type Output;
    fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;
}

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

2.4.2、使用 Waker 唤醒任务

对于 Future 来说,第一次被 poll 时完成任务是很正常的,但是需要确保在未来一旦准备好时,可以通过执行器再次对其进行 poll 进而继续往下执行,改通知就是通过 Waker 类型完成。

Waker 提供了一个 wake() 方法可以用于告诉执行器:相关的任务可以被唤醒了,此时执行器就可以对相应的 Future 再次进行 poll 操作。

2.4.3、执行器 Executor

Future 是惰性的,除非驱动它们来完成,否则就什么都不做,一种驱动方式就是在 async 函数里面使用 .awai,但是只是把问题推到上一层面。

Future 执行者会获取一系列顶层的 Future,通过在 Future 可以有进展到时候调用 poll,来将这些 Future 运行至完成;

通常首选,执行者将 poll () 一个 Future 一次;

当 Future 通过调用 wake() 表示它们已经准备好取得进展时,它们就会被放回到一个队列里,然后 poll 再次被调用,重复此操作直到 Future 完成。

2.4.4、执行者和系统I/O

Rust 通过跨平台包 mio 来使用,借助 IO 多路复用机制实现。

2.5、async 和 .await

2.5.1、什么是async 和 .await

async 和 .await 是 Rust 的特殊语法,在发生阻塞的时候,她会放弃当前线程的控制权称为可能,这就是允许在等待操作完成的时候,允许其他代码取得进展;

2.5.2、使用 async 的三种方式

1、方式一: async fn

async fn foo() -> u8{ 5 }

2、方式二:async blocks

use std::future::Future;

async fn foo() -> u8{ 5 }

fn bar() -> impl Future<Output= u8>{

    async{
        let x :u8 = foo().await;
        x + 5
    }
}

3、async 闭包

fn baz() -> impl Future<Output = u8> {
    let closure = async |x: u8| {
        await!(bar()) + x
    };
    closure(5)
}

1、async fn 、 async block、async 闭包 ,都返回实现了 Future trait 的值
2、async block 和 其他 future 都是惰性的;
3、使用 .await 是最常见的运行 future 的方式

2.5.3、async 生命周期

async fn 与传统函数不同,带引用或其他非 'static参数的,返回一个受参数生命周期限制的 Future。


async fn foo(x: &u8) -> u8 { *x }

fn foo<'a>(x: &'a u8) -> impl Future<Output = ()> + 'a {
    async { *x }
}

2.5.4、async move

async 块和闭包 允许 move 关键字,就像闭包一样,一个async move 块将获取其他引用的所有权,允许它获得比目前的范围长,但放弃了其他代码分析那些变量的能

fn foo() -> impl Future<Output = ()> {
    let my_string = "foo".to_string();
    async move {
        ...
        println!("{}", my_string);
    }
}

2.6、Pin 和 Unpin

2.6.1、Pin

为了针对 Future 进行论序,必须使用 Pin 。在 Rust 里面所有的类型都可以分为两种类型。

  • 类型的值可以在内存中安全的被移动,例如 数值、字符串、布尔值、结构体、枚举等等
  • 自引用类型
let fut_one = /* ... */; // Future 1
let fut_two = /* ... */; // Future 2
async move {
    fut_one.await;
    fut_two.await;
}

我们使用 Pin 可以对对象进行固定,可以保证对象不会移动。

async {
    let mut x = [0; 128];
    let read_into_buf_fut = read_into_buf(&mut x);
    await!(read_into_buf_fut);
    println!("{:?}", x);
}

Pin 是一个结构体,包裹一个指针,并且能确保指针指向的数据不会被移动。

pub struct Pin<P> {
    pointer: P,
}

2.6.2、Unpin

事实上,绝大数类型都不在意是否被移动,因为他们都自动实现了 Unpin trait。 可以被 Pin 住的值实现的特征都是 !Unpin

2.6.3、Pin 在实践中的运用

2.6.3.1、将值固定到栈上

我们可以使用 Pin 来解决指针指向数据被移动的问题。

use std::pin::Pin;
use std::marker::PhantomPinned;

#[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
    _marker: PhantomPinned,
}


impl Test {
    fn new(txt: &str) -> Self {
        Test {
            a: String::from(txt),
            b: std::ptr::null(),
            _marker: PhantomPinned, // 这个标记可以让我们的类型自动实现特征`!Unpin`
        }
    }

    fn init(self: Pin<&mut Self>) {
        let self_ptr: *const String = &self.a;
        let this = unsafe { self.get_unchecked_mut() };
        this.b = self_ptr;
    }

    fn a(self: Pin<&Self>) -> &str {
        &self.get_ref().a
    }

    fn b(self: Pin<&Self>) -> &String {
        assert!(!self.b.is_null(), "Test::b called without Test::init being called first");
        unsafe { &*(self.b) }
    }
}

使用PhantomPinned 将自定义结构体 Test 变成了 !Unpin ,因此该结构体无法在被移动。

一旦类型实现了 !Unpin ,那将它的值固定到栈( stack )上就是不安全的行为,因此在代码中我们使用了 unsafe 语句块来进行处理,你也可以使用 pin_utils 来避免 unsafe 的使用。

此时在尝试移动被固定的值,就会导致编译错误;

2.6.3.2、固定到堆上

将一个 !Unpin 类型的值固定到堆上,会给予该值一个稳定的内存地址,它指向的堆中的值在 Pin 后是无法被移动的。而且与固定在栈上不同,我们知道堆上的值在整个生命周期内都会被稳稳地固定住。

use std::pin::Pin;
use std::marker::PhantomPinned;

#[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
    _marker: PhantomPinned,
}

impl Test {
    fn new(txt: &str) -> Pin<Box<Self>> {
        let t = Test {
            a: String::from(txt),
            b: std::ptr::null(),
            _marker: PhantomPinned,
        };
        let mut boxed = Box::pin(t);
        let self_ptr: *const String = &boxed.as_ref().a;
        unsafe { boxed.as_mut().get_unchecked_mut().b = self_ptr };

        boxed
    }

    fn a(self: Pin<&Self>) -> &str {
        &self.get_ref().a
    }

    fn b(self: Pin<&Self>) -> &String {
        unsafe { &*(self.b) }
    }
}

pub fn main() {
    let test1 = Test::new("test1");
    let test2 = Test::new("test2");

    println!("a: {}, b: {}",test1.as_ref().a(), test1.as_ref().b());
    println!("a: {}, b: {}",test2.as_ref().a(), test2.as_ref().b());
}

2.6.3.3、将固定住的 Future 变为 Unpin

所有 async 函数返回的 Future 默认都是 !Unpin 的,但是实际应用中,一些函数会要求它们处理的 Future 是 Unpin的,此时若你使用的 Future 是 !Unpin 的,必须要使用以下的方法先将 Future 进行固定。

  • 使用 Box::pin 创建一个 Pin>
  • pin_utils::pin_mut!, 创建一个 Pin<&mut T>

固定后的 Pin<&mut T>Pin> 既可以用于 Future ,又会自动实现 Unpin。

use pin_utils::pin_mut; // `pin_utils` 可以在crates.io中找到

// 函数的参数是一个`Future`,但是要求该`Future`实现`Unpin`
fn execute_unpin_future(x: impl Future<Output = ()> + Unpin) { /* ... */ }

let fut = async { /* ... */ };
// 下面代码报错: 默认情况下,`fut` 实现的是`!Unpin`,并没有实现`Unpin`
// execute_unpin_future(fut);

// 使用`Box`进行固定
let fut = async { /* ... */ };
let fut = Box::pin(fut);
execute_unpin_future(fut); // OK

// 使用`pin_mut!`进行固定
let fut = async { /* ... */ };
pin_mut!(fut);
execute_unpin_future(fut); // OK

2.7、Stream

2.7.1、Stream

Stream 特征类似于 Future Trait ,但是前者在完成前可以生成多个值,这种行为跟标准库中的 Iterator trait 倒是颇为相似。

trait Stream {
    // Stream生成的值的类型
    type Item;

    // 尝试去解析Stream中的下一个值,
    // 若无数据,返回`Poll::Pending`, 若有数据,返回 `Poll::Ready(Some(x))`, `Stream`完成则返回 `Poll::Ready(None)`
    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>)
        -> Poll<Option<Self::Item>>;
}

poll_next 函数有三种可能的返回值

  • Poll::Pending 说明下一个值可还没就绪,仍然需要等待
  • Poll::Ready(Some(val)) 已经就绪,成功返回一个值,程序可以通过 poll_next 在获取下一个值;
  • Poll::Ready(None) 表示 Stream 已经结束

关于 Stream 的一个常见例子 是消息通道的消费者。每次有消息从发送端发送后,他都可以接收到一个 Some(val) 值,一旦 Send 端关闭(drop),且消息通道中没有消息后,他会接受到一个 None 值。

async fn send_recv() {
    const BUFFER_SIZE: usize = 10;
    let (mut tx, mut rx) = mpsc::channel::<i32>(BUFFER_SIZE);

    tx.send(1).await.unwrap();
    tx.send(2).await.unwrap();
    drop(tx);

    // `StreamExt::next` 类似于 `Iterator::next`, 但是前者返回的不是值,而是一个 `Future>`,
    // 因此还需要使用`.await`来获取具体的值
    assert_eq!(Some(1), rx.next().await);
    assert_eq!(Some(2), rx.next().await);
    assert_eq!(None, rx.next().await);
}

2.7.2、迭代和并发

  • 与同步的 Iterator 类似,有很多种方法迭代和处理 Stream 中的值:
    • 组合器风格:map、filter、fold;
    • 相应的 “early-exit-on-error” 版本:try_map、try_filter、try_fold;
  • for 循环无法和 Stream 一起使用
  • 命令式的 while let 和 next/try_next 函数可以与 Stream 一起使用
async fn sum_with_next(mut stream: Pin<&mut dyn Stream<Item = i32>>) -> i32 {
    use futures::stream::StreamExt; // 引入 next
    let mut sum = 0;
    while let Some(item) = stream.next().await {
        sum += item;
    }
    sum
}

async fn sum_with_try_next(
    mut stream: Pin<&mut dyn Stream<Item = Result<i32, io::Error>>>,
) -> Result<i32, io::Error> {
    use futures::stream::TryStreamExt; // 引入 try_next
    let mut sum = 0;
    while let Some(item) = stream.try_next().await? {
        sum += item;
    }
    Ok(sum)
}

如果你选择一次处理一个值的模式,可能会造成无法并发,这就失去了异步编程的意义。

async fn jump_around(
    mut stream: Pin<&mut dyn Stream<Item = Result<u8, io::Error>>>,
) -> Result<(), io::Error> {
    use futures::stream::TryStreamExt; // 引入 `try_for_each_concurrent`
    const MAX_CONCURRENT_JUMPERS: usize = 100;

    stream.try_for_each_concurrent(MAX_CONCURRENT_JUMPERS, |num| async move {
        jump_n_times(num).await?;
        report_n_jumps(num).await?;
        Ok(())
    }).await?;

    Ok(())
}

2.8、join!和 select!

真正的异步应用通常需要同时执行几个不同的操作,可同时执行多个异步的操作的方式

  • join!:等待所有 Future 完成
  • select! : 等待多个 Future 中的一个完成
  • Spawning:创建一个顶级任务,他会运行一个 Future 直到完成
  • FuturesUnordered:一组 Future ,它们会产生每个子 Future 的结果

2.8.1、join!

使用 join! 宏可以等待所有的 Future 完成;

async fn  async_main(){
    let f1= learn_and_sing_sone();
    let f2 = dance();

    futures::join!(f1, f2);
}

2.8.2、 try_join!

对于返回 Result 的 Future ,更考虑使用 try_join! 如果一个 Future 中的某一个返回了错误, try_join! 会立即完成。

use futures::try_join;

async fn get_book() -> Result<Book, String> { /* ... */ Ok(Book) }
async fn get_music() -> Result<Music, String> { /* ... */ Ok(Music) }

async fn get_book_and_music() -> Result<(Book, Music), String> {
    let book_fut = get_book();
    let music_fut = get_music();
    try_join!(book_fut, music_fut)
}

有一点需要注意,传给 try_join! 的所有 Future 都必须拥有相同的错误类型。如果错误类型不同,可以考虑使用来自 futures::future::TryFutureExt 模块的 map_err 和 err_info 方法将错误进行转换:

use futures::{
    future::TryFutureExt,
    try_join,
};

async fn get_book() -> Result<Book, ()> { /* ... */ Ok(Book) }
async fn get_music() -> Result<Music, String> { /* ... */ Ok(Music) }

async fn get_book_and_music() -> Result<(Book, Music), String> {
    let book_fut = get_book().map_err(|()| "Unable to get book".to_string());
    let music_fut = get_music();
    try_join!(book_fut, music_fut)
}

2.8.3、 select!

可以同时运行多个 Future,允许用户在任意 Future 完成时进行响应。join! 只有等所有 Future 结束后,才能集中处理结果,如果你想同时等待多个 Future ,且任何一个 Future 结束后,都可以立即被处理,可以考虑使用 futures::select!:

use futures::{
    future::FutureExt, // for `.fuse()`
    pin_mut,
    select,
};

async fn task_one() { /* ... */ }
async fn task_two() { /* ... */ }

async fn race_tasks() {
    let t1 = task_one().fuse();
    let t2 = task_two().fuse();

    pin_mut!(t1, t2);

    select! {
        () = t1 => println!("任务1率先完成"),
        () = t2 => println!("任务2率先完成"),
    }
}

上面的代码会同时并发地运行 t1 和 t2, 无论两者哪个先完成,都会调用对应的 println! 打印相应的输出,然后函数结束且不会等待另一个任务的完成。

但是,在实际项目中,我们往往需要等待多个任务都完成后,再结束,像上面这种其中一个任务结束就立刻结束的场景着实不多。

2.8.3.1、default => … 和 complete =>…

select! 支持 default 和 complete 分支

  • default:如果选中的 Future 尚未完成,就会运行 default 分支,拥有default 的 selext 总是会立即返回
  • complete:它用户所有选中的 future 都已完成的情况,往往配合 loop 使用,loop 用于循环完成所有的 Future
use futures::future;
use futures::select;
pub fn main() {
    let mut a_fut = future::ready(4);
    let mut b_fut = future::ready(6);
    let mut total = 0;

    loop {
        select! {
            a = a_fut => total += a,
            b = b_fut => total += b,
            complete => break,
            default => panic!(), // 该分支永远不会运行,因为 `Future` 会先运行,然后是 `complete`
        };
    }
    assert_eq!(total, 10);
}

以上代码 default 分支由于最后一个运行,而在它之前 complete 分支已经通过 break 跳出了循环,因此 default 永远不会被执行。

如果你希望 default 也有机会露下脸,可以将 complete 的 break 修改为其它的,例如 println!(“completed!”),然后再观察下运行结果

2.8.3.2、与 Unpin 和 FusedFuture 交互

首先,.fuse() 方法可以让 Future 实现 FusedFuture 特征, 而 pin_mut! 宏会为 Future 实现 Unpin 特征,这两个特征恰恰是使用 select 所必须的:

  • Unpin,由于 select 不会通过拿走所有权的方式使用 Future,而是通过可变引用的方式去使用,这样当 select 结束后,该 Future 若没有被完成,它的所有权还可以继续被其它代码使用。
  • FusedFuture 的原因跟上面类似,当 Future 一旦完成后,那 select 就不能再对其进行轮询使用。Fuse 意味着熔断,相当于 Future 一旦完成,再次调用 poll 会直接返回 Poll::Pending。

只有实现了 FusedFuture,select 才能配合 loop 一起使用。假如没有实现,就算一个 Future 已经完成了,它依然会被 select 不停的轮询执行。

Stream 稍有不同,它们使用的特征是 FusedStream。 通过 .fuse()(也可以手动实现)实现了该特征的 Stream,对其调用 .next() 或 .try_next() 方法可以获取实现了 FusedFuture 特征的Future:

use futures::{
    stream::{Stream, StreamExt, FusedStream},
    select,
};

async fn add_two_streams(
    mut s1: impl Stream<Item = u8> + FusedStream + Unpin,
    mut s2: impl Stream<Item = u8> + FusedStream + Unpin,
) -> u8 {
    let mut total = 0;

    loop {
        let item = select! {
            x = s1.next() => x,
            x = s2.next() => x,
            complete => break,
        };
        if let Some(next_num) = item {
            total += next_num;
        }
    }

    total
}

2.8.3.3、在 select 循环并发

一个很实用但又鲜为人知的函数是 Fuse::terminated() ,可以使用它构建一个空的 Future ,空自然没啥用,但是如果它能在后面再被填充呢?

考虑以下场景:当你要在 select 循环中运行一个任务,但是该任务却是在 select 循环内部创建时,上面的函数就非常好用了。

use futures::{
    future::{Fuse, FusedFuture, FutureExt},
    stream::{FusedStream, Stream, StreamExt},
    pin_mut,
    select,
};

async fn get_new_num() -> u8 { /* ... */ 5 }

async fn run_on_new_num(_: u8) { /* ... */ }

async fn run_loop(
    mut interval_timer: impl Stream<Item = ()> + FusedStream + Unpin,
    starting_num: u8,
) {
    let run_on_new_num_fut = run_on_new_num(starting_num).fuse();
    let get_new_num_fut = Fuse::terminated();
    pin_mut!(run_on_new_num_fut, get_new_num_fut);
    loop {
        select! {
            () = interval_timer.select_next_some() => {
                // 定时器已结束,若`get_new_num_fut`没有在运行,就创建一个新的
                if get_new_num_fut.is_terminated() {
                    get_new_num_fut.set(get_new_num().fuse());
                }
            },
            new_num = get_new_num_fut => {
                // 收到新的数字 -- 创建一个新的`run_on_new_num_fut`并丢弃掉旧的
                run_on_new_num_fut.set(run_on_new_num(new_num).fuse());
            },
            // 运行 `run_on_new_num_fut`
            () = run_on_new_num_fut => {},
            // 若所有任务都完成,直接 `panic`, 原因是 `interval_timer` 应该连续不断的产生值,而不是结束
            //后,执行到 `complete` 分支
            complete => panic!("`interval_timer` completed unexpectedly"),
        }
    }
}

当某个 Future 有多个拷贝都需要同时运行时,可以使用 FuturesUnordered 类型。下面的例子跟上个例子大体相似,但是它会将 run_on_new_num_fut 的每一个拷贝都运行到完成,而不是像之前那样一旦创建新的就终止旧的。

use futures::{
    future::{Fuse, FusedFuture, FutureExt},
    stream::{FusedStream, FuturesUnordered, Stream, StreamExt},
    pin_mut,
    select,
};

async fn get_new_num() -> u8 { /* ... */ 5 }

async fn run_on_new_num(_: u8) -> u8 { /* ... */ 5 }


// 使用从 `get_new_num` 获取的最新数字 来运行 `run_on_new_num`
//
// 每当计时器结束后,`get_new_num` 就会运行一次,它会立即取消当前正在运行的`run_on_new_num` ,
// 并且使用新返回的值来替换
async fn run_loop(
    mut interval_timer: impl Stream<Item = ()> + FusedStream + Unpin,
    starting_num: u8,
) {
    let mut run_on_new_num_futs = FuturesUnordered::new();
    run_on_new_num_futs.push(run_on_new_num(starting_num));
    let get_new_num_fut = Fuse::terminated();
    pin_mut!(get_new_num_fut);
    loop {
        select! {
            () = interval_timer.select_next_some() => {
                 // 定时器已结束,若 `get_new_num_fut` 没有在运行,就创建一个新的
                if get_new_num_fut.is_terminated() {
                    get_new_num_fut.set(get_new_num().fuse());
                }
            },
            new_num = get_new_num_fut => {
                 // 收到新的数字 -- 创建一个新的 `run_on_new_num_fut` (并没有像之前的例子那样丢弃掉旧值)
                run_on_new_num_futs.push(run_on_new_num(new_num));
            },
            // 运行 `run_on_new_num_futs`, 并检查是否有已经完成的
            res = run_on_new_num_futs.select_next_some() => {
                println!("run_on_new_num_fut returned {:?}", res);
            },
            // 若所有任务都完成,直接 `panic`, 原因是 `interval_timer` 应该连续不断的产生值,而不是结束
            //后,执行到 `complete` 分支
            complete => panic!("`interval_timer` completed unexpectedly"),
        }
    }
}

2.9、async 语句中使用?

async 语句和 async fn 最大的区别就是前者无法显示的声明返回值,在大多数时候这都不是问题,但是配合?一起使用的时候,就有不同;

async fn foo() -> Result<u8, String> {
    Ok(1)
}
async fn bar() -> Result<u8, String> {
    Ok(1)
}
pub fn main() {
    let fut = async {
        foo().await?;
        bar().await?;
        Ok(())
    };
}

当我们在 async 语句块里面使用?的时候,会报如下错误

error[E0282]: type annotations needed
  --> src/main.rs:14:9
   |
11 |     let fut = async {
   |         --- consider giving `fut` a type
...
14 |         Ok(1)
   |         ^^ cannot infer type for type parameter `E` declared on the enum `Result`

原因是因为编译器无法推断出 Result 中的E的类型,而且编译器的提示也不是可信的。如果要解决编译器无法推断类型的问题,我们就可以使用手动去添加类型注释的方式

let fut = async {
    foo().await?;
    bar().await?;
    Ok::<(), String>(()) // 在这一行进行显式的类型注释
};

2.10、async 函数 和 Send 特征

我们之前讲解过 Send 特征对于多线程之间数据传递的重要性,对于 async fn 也一样,返回的 future 是否在线程间传递的关键在于 .await 运行过程中,作用域的变量类型是否是 Send。

2.11、递归使用 async fn

在内部实现中,async fn 被编译成一个状态机,这会导致递归使用 async fn 变得较为复杂,因为编译后的状态及还要包含自身。

这是一种典型的动态大小类型,他的大小会无限增长,因此编译器会直接报错。

error[E0733]: recursion in an `async fn` requires boxing
 --> src/lib.rs:1:22
  |
1 | async fn recursive() {
  |                      ^ an `async fn` cannot invoke itself directly
  |
  = note: a recursive `async fn` must be rewritten to return a boxed future.

我们只需要将其使用Box 放到堆上,就可以解决这个问题;

use futures::future::{BoxFuture, FutureExt};

fn recursive() -> BoxFuture<'static, ()> {
    async move {
        recursive().await;
        recursive().await;
    }.boxed()
}

2.12、在特种中使用 async

在目前的版本中,还无法在特征中敌营 async fn 函数

trait Test {
    async fn test();
}

报错信息如下

error[E0706]: functions in traits cannot be declared `async`
 --> src/main.rs:4:5
  |
4 |     async fn test();
  |     -----^^^^^^^^^^^
  |     |
  |     `async` because of this
  |
  = note: `async` trait functions are not currently supported
  = note: consider using the `async-trait` crate: https://crates.io/crates/async-trait
  = note: see issue #91611 <https://github.com/rust-lang/rust/issues/91611> for more information

编译器给出了我们提示,我们可以使用 async-trait 来解决这个问题

cargo add async-trait
use async_trait::async_trait;

#[async_trait]
trait Advertisement {
    async fn run(&self);
}

struct Modal;

#[async_trait]
impl Advertisement for Modal {
    async fn run(&self) {
        self.render_fullscreen().await;
        for _ in 0..4u16 {
            remind_user_to_join_mailing_list().await;
        }
        self.hide_for_now().await;
    }
}

struct AutoplayingVideo {
    media_url: String,
}

#[async_trait]
impl Advertisement for AutoplayingVideo {
    async fn run(&self) {
        let stream = connect(&self.media_url).await;
        stream.play().await;

        // 用视频说服用户加入我们的邮件列表
        Modal.run().await;
    }
}

不过每一次特征中的async 函数被调用时,都会产生一堆内存分配,对于大多数场景,这个性能开销可以接受,但是函数一秒调用几十万、几百万次就要小心这一部分的性能的问题。

你可能感兴趣的:(跟小嘉学,Rust,编程,rust,开发语言,后端)