【跟小嘉学 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)
本章节讲解 Rust 的异步编程方案,我们将讲解 Future、Waker、Executor、Pin、async 和 await、Stream等
主要教材参考 《The Rust Programming Language》
主要教材参考 《Rust For Rustaceans》
主要教材参考 《The Rustonomicon》
主要教材参考 《Rust 高级编程》
主要教材参考 《Cargo 指南》
主要教材参考 《Rust 异步编程》
当多个线程访问某一共享资源时,可能会发生死锁。死锁发生后,所有参与的线程都无法访问数据。
死锁发生有四个条件
Rust 的标准库提供了 线程 std::thread ,每个线程都有自己的栈和状态,使用闭包指定线程的行为
use std::thread;
fn main() {
thread::spawn(||{
println!("Hello, World!");
});
}
thread::spawn 返回 JoinHandler 类型的线程句柄 (handlers)。
use std::thread;
fn main() {
let handler = thread::spawn(||{
println!("Hello, World!");
});
println!("{:?}", handler.join());
}
当线程的句柄杯丢弃的时候,线程会变为失联(detached)状态。此时它还在运行,不能克隆线程句柄,只有一个变量有权限来做线程的汇合(join)。
如果线程发生 panic,那么将无法从该线程内部进行恢复。
Rust 的线程如果发生恐慌,和创建这个线程的线程没有关系。
如果主线程恐慌或者正常结束,其他线程也会被终止。
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();
}
Rust 类型系统包含了满足并发承诺的特征
如果一个线程锁定了一个互斥锁,然后发生了 panic ,此时互斥锁会进入中毒(poisoned) 状态,因为这个锁不会被释放了。
ock 方法返回一个 LockResult。
通道(channel)可以用来同步线程之间的状态,通道能够在线程之间传递数据,可以用来提醒其他线程关于数据已经就绪、事件已经发生等情况。
实现多生产者、单消费者的通信功能(Muliti-Producer, Single-Consumer)。
主要设计三种类型
Sender :用于给 Receiver 发送数据,可以克隆,交给多个线程实现多生产者
SyncSender:用于给 Receiver 发送数据
Receiver:不能克隆,因此是单消费者
使用 channel 函数创建一对链接,Sender 是异步通道,发送数据的时候不会阻塞发生线程。
使用 sync_channel 函数可以创建同步通道,发送消息的时候会阻塞;
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().
}
/// 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
是 Result
Option>
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<()> { ... }
}
BufReader 可以给任意的读取对象添加缓冲机制,实现了 Read 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
}
BufWriter 实现了 Write 的trait,直接缓存所有写入的数据,在失效前会把所有缓存的数据写出。
BufWriter 没有像 BufReader 那样实现新的特型,他直接缓存所有的写入的数据,在失效前把所有缓存的数据写出。
let lock: io::StdinLock = io::stdin().lock();
异步编程是一种并发编程模型,在很多语言里都支持。允许使用少量的操作想听线程来处理一堆并发任务
OS Thread:不需要改动编程模型,这使得并发变得简单很多。但是多线程异步编程则变得困难许多,同时性能开销上也会变大,毕竟需要开很多线程。我们之前学的线程池可以缓和部分性能开销,但是对于大规模密集型工作来说还是不够的。
事件驱动编程:搭配callbacks也就是回调,性价比高很多,但是代价则是冗长繁杂的写法、非线性(non-linear)工作流(比如前端的回调地狱)。以及很难跟踪数据和错误产生的地点。
Coroutines:和多线程类似,并不需要改动编程模型,这也就意味着用起来比较简单。同时它也和异步类似,可以支持大量的任务。而代价则是高度抽象,一些重要的底层细节都不会暴露出来。这也就意味着你想去自定义一些较底层的特性是很难实现的。
Actor模型: actors指的是将所有的并发计算切割成一小个一小个的单元(unit),这些个单元通过易错信息(fallible message)来沟通,比较像分布式系统(distributed systems)。它可以有效的实施,但是它还有一些实用性理论比如控制流(control flow)和重试逻辑(retry logic)还没完善。
注意:异步 并不一定比线程好;
在 Rust 中如果不想使用异步,那么一般替代方案就是 OS Thread了。
如果你项目并发量不大,也就是任务比较少,那么首选 OS Thread了。这些线程都是来自 CPU 的,所有会有内存开销。创建/切换线程是非常昂贵的行为,即使是空闲线程也会吃资源
这对于驱动器(drivers)和其他延迟灵敏的应用(latency sensitive)的应用
异步则可以明显的减少对于CPU和内存的开销,尤其是在大规模密集型任务中,比如服务器、数据库。
代价就是大量的 binary blob(二进制斑点),他们来自 async function 生成的状态机(state matchines)。因为每个可执行文件都被捆绑到一个异步 runtime 上了。
Rust 异步特性目前大部分稳定,另一些特性还在完善之中。
特点
Rust 本身支持异步编程,但是大多数异步应用程序依赖于社区提供的功能。
添加依赖
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 或者其他的执行者执行。
在 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");
}
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 保持了一致。
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}");
}
}
Future trait 是 Rust 异步编程的核心,毕竟异步函数是异步编程的核心,而 Future 恰恰是异步函数的返回值和被执行的关键。
trait SimpleFuture {
type Output;
fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;
}
enum Poll<T> {
Ready(T),
Pending,
}
对于 Future 来说,第一次被 poll 时完成任务是很正常的,但是需要确保在未来一旦准备好时,可以通过执行器再次对其进行 poll 进而继续往下执行,改通知就是通过 Waker 类型完成。
Waker 提供了一个 wake() 方法可以用于告诉执行器:相关的任务可以被唤醒了,此时执行器就可以对相应的 Future 再次进行 poll 操作。
Future 是惰性的,除非驱动它们来完成,否则就什么都不做,一种驱动方式就是在 async 函数里面使用 .awai
,但是只是把问题推到上一层面。
Future 执行者会获取一系列顶层的 Future,通过在 Future 可以有进展到时候调用 poll,来将这些 Future 运行至完成;
通常首选,执行者将 poll () 一个 Future 一次;
当 Future 通过调用 wake() 表示它们已经准备好取得进展时,它们就会被放回到一个队列里,然后 poll 再次被调用,重复此操作直到 Future 完成。
Rust 通过跨平台包 mio 来使用,借助 IO 多路复用机制实现。
async 和 .await 是 Rust 的特殊语法,在发生阻塞的时候,她会放弃当前线程的控制权称为可能,这就是允许在等待操作完成的时候,允许其他代码取得进展;
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 的方式
async fn 与传统函数不同,带引用或其他非 'static
参数的,返回一个受参数生命周期限制的 Future。
async fn foo(x: &u8) -> u8 { *x }
fn foo<'a>(x: &'a u8) -> impl Future<Output = ()> + 'a {
async { *x }
}
async 块和闭包 允许 move 关键字,就像闭包一样,一个async move 块将获取其他引用的所有权,允许它获得比目前的范围长,但放弃了其他代码分析那些变量的能
fn foo() -> impl Future<Output = ()> {
let my_string = "foo".to_string();
async move {
...
println!("{}", my_string);
}
}
为了针对 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,
}
事实上,绝大数类型都不在意是否被移动,因为他们都自动实现了 Unpin trait。 可以被 Pin 住的值实现的特征都是 !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) -> 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 的使用。
此时在尝试移动被固定的值,就会导致编译错误;
将一个 !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());
}
所有 async 函数返回的 Future 默认都是 !Unpin 的,但是实际应用中,一些函数会要求它们处理的 Future 是 Unpin的,此时若你使用的 Future 是 !Unpin 的,必须要使用以下的方法先将 Future 进行固定。
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
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 函数有三种可能的返回值
关于 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);
}
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(())
}
真正的异步应用通常需要同时执行几个不同的操作,可同时执行多个异步的操作的方式
使用 join! 宏可以等待所有的 Future 完成;
async fn async_main(){
let f1= learn_and_sing_sone();
let f2 = dance();
futures::join!(f1, f2);
}
对于返回 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)
}
可以同时运行多个 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! 打印相应的输出,然后函数结束且不会等待另一个任务的完成。
但是,在实际项目中,我们往往需要等待多个任务都完成后,再结束,像上面这种其中一个任务结束就立刻结束的场景着实不多。
select! 支持 default 和 complete 分支
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!”),然后再观察下运行结果
首先,.fuse() 方法可以让 Future 实现 FusedFuture 特征, 而 pin_mut! 宏会为 Future 实现 Unpin 特征,这两个特征恰恰是使用 select 所必须的:
只有实现了 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
}
一个很实用但又鲜为人知的函数是 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"),
}
}
}
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>(()) // 在这一行进行显式的类型注释
};
我们之前讲解过 Send 特征对于多线程之间数据传递的重要性,对于 async fn 也一样,返回的 future 是否在线程间传递的关键在于 .await 运行过程中,作用域的变量类型是否是 Send。
在内部实现中,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()
}
在目前的版本中,还无法在特征中敌营 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 函数被调用时,都会产生一堆内存分配,对于大多数场景,这个性能开销可以接受,但是函数一秒调用几十万、几百万次就要小心这一部分的性能的问题。