常见的并发模型
- OS 线程, 它非常适合作为语言的原生并发模型,缺点是线程间同步困难,线程间的上下文切换损耗较大。
- 事件驱动(Event driven), 模型性能相当的好,但是存在回调地狱的风险
- 协程(Coroutines) ,无需改变编程模型,支持大量的任务并发运行,但协程抽象层次过高
- actor 模型是 erlang 的杀手锏之一,相对来说比较容易实现,但遇到流控制、失败重试等场景时,不太好用。
- async/await, 该模型性能高,还能支持底层编程,同时又像线程和协程那样无需过多的改变编程模型,但
async
模型内部实现机制过于复杂。
Rust 同时提供多线程编程和 async 编程,前者通过标准库实现,有大量 CPU
密集任务需要并行运行时,例如并行计算,选多线程模型。后者通过语言特性 + 标准库 + 三方库的方式实现,在你需要高并发、异步 I/O
时,选择它。Async 在 Rust 中使用开销是零。
Rust没有内置完整的特性和运行时,而是由标准库提供所必须的特征(例如 Future
)、类型和函数,还有一些实用的类型、宏和函数由官方开发的 futures 包提供。关键字 async/await
由 Rust 语言提供。社区提供 async
运行时的支持 ,比如:tokio 和 async-std。Rust 不允许在特征中声明 async
函数。
同步和异步代码使用不同的设计模式,我们无法在一个同步函数中去调用一个 async
异步函数
编译器会为每一个async
函数生成状态机,这会导致在栈跟踪时会包含这些状态机的细节,同时还包含了运行时对函数的调用,因此,栈跟踪记录(例如 panic
时)将变得更加难以解读
通过 async
标记的语法块会被转换成实现了Future
特征的状态机。使用 async fn
语法来创建一个异步函数,异步函数的返回值是一个 Future。在使用之前需要先在Cargo.toml文件中
先引入 futures
包。`block_on`会阻塞当前线程直到指定的`Future`执行完成。在async fn
函数中使用.await
可以等待另一个异步调用的完成。但是与block_on
不同,.await
并不会阻塞当前的线程
//Cargo.toml
[dependencies]
futures = "0.3"
use futures::executor::block_on;
struct Song {
author: String,
name: String,
}
async fn learn_song() -> Song {
Song {
author: "曲婉婷".to_string(),
name: String::from("《我的歌声里》"),
}
}
async fn sing_song(song: Song) {
println!(
"给大家献上一首{}的{} ~ {}",
song.author, song.name, "你存在我深深的脑海里~ ~"
);
}
async fn dance() {
println!("唱到情深处,身体不由自主的动了起来~ ~");
}
async fn learn_and_sing() {
// 这里使用`.await`来等待学歌的完成,但是并不会阻塞当前线程,该线程在学歌的任务`.await`后,完全可以去执行跳舞的任务
let song = learn_song().await;
// 唱歌必须要在学歌之后
sing_song(song).await;
}
async fn async_main() {
let f1 = learn_and_sing();
let f2 = dance();
// `join!`可以并发的处理和等待多个`Future`,若`learn_and_sing Future`被阻塞,那`dance Future`可以拿过线程的所有权继续执行。若`dance`也变成阻塞状态,那`learn_and_sing`又可以再次拿回线程所有权,继续执行。
// 若两个都被阻塞,那么`async main`会变成阻塞状态,然后让出线程所有权,并将其交给`main`函数中的`block_on`执行器
futures::join!(f1, f2);
}
fn main() {
block_on(async_main());
}
输出结果:
给大家献上一首曲婉婷的《我的歌声里》 ~ 你存在我深深的脑海里~ ~
唱到情深处,身体不由自主的动了起来~ ~
异步是在单线程(Main线程)上做的异步,是在一个线程中运行多个future(task),调用 future_A 的 await 方法并不会阻塞语言线程A,此时的语言线程A还可以继续执行 future_B,C,D.。
Pin与Unpin
Pin可以防止一个类型在内存中被移动,比如自引用类型
struct SelfRef {
value: String,
pointer_to_value: *mut String,
}
SelfRef结构体中,pointer_to_value
是一个裸指针,指向第一个字段 value
持有的字符串 String
,如果value
的内存地址变了,而 pointer_to_value
依然指向 value
之前的地址,则会出现致命问题,此时就需要Pin,而与之相反的UnPin
表示类型可以在内存中安全地移动。
Pin是一个结构体,它包裹一个指针,能确保该指针指向的数据不会被移动,例如 Pin<&mut T>
pub struct Pin<P> {
pointer: P,
}
Unpin是一个特征,表明一个类型可以随意被移动,一个类型如果不能被移动,它必须实现 !Unpin
特征, !Unpin
表示类型没有实现 Unpin
特征,反过来则不成立,即如果实现了 Unpin
特征,也可能被 Pin,知识没有任何效果而已,如Pin<&mut u8>
跟 &mut u8
实际上并无区别,一样可以被移动。
对自引用类型中的数据进行移动的一个例子:
#[derive(Debug)]
struct Test {
a: String,
b: *const String,
}
impl Test {
fn new(txt: &str) -> Self {
Test {
a: String::from(txt),
b: std::ptr::null(),
}
}
fn init(&mut self) {
let self_ref: *const String = &self.a;
self.b = self_ref;
}
fn a(&self) -> &str {
&self.a
}
fn b(&self) -> &String {
assert!(!self.b.is_null(), "Test::b called without Test::init being called first");
unsafe { &*(self.b) }
}
}
fn main() {
let mut test1 = Test::new("test1");
test1.init();
let mut test2 = Test::new("test2");
test2.init();
println!("a: {}, b: {}", test1.a(), test1.b());
//std::mem::swap(&mut test1, &mut test2);*/
println!("a: {}, b: {}", test2.a(), test2.b());
}
正常输出:
a: test1, b: test1
a: test2, b: test2
/* 移动后的输出:
a: test1, b: test1
a: test1, b: test2 */
Test
是一个自引用结构体,它提供了用于获取字段 a
和 b
的值的引用。这里b
是 a
的一个引用.
出现第二种输出的原因是 test2.b
指针依然指向了旧的地址,而该地址对应的值现在在 test1
里,
图解如下:
在实际应用中,大部分函数处理的 Future
是 Unpin
的,若使用的 Future
是 !Unpin
的,比如async
函数返回的 Future
默认就是 !Unpin,则
必须要使用Box::pin来
创建一个 Pin<Box<T>>或pin_utils::pin_mut!
, 创建一个 Pin<&mut T>
。以将 Future
进行固定,固定后获得的 Pin<Box<T>>
和 Pin<&mut T>
既可以用于 Future
,又会自动实现 Unpin
。可使用std::marker::PhantomPinned来为自己的类型添加!Unpin约束,我们可以通过unsafe将!Unpin值固定到栈上(因为
一旦类型实现了 !Unpin
,那将它的值固定到栈上就是不安全的行为,因此需要使用了 unsafe
语句块),也可以通过Box::pin
将!Unpin
类型的值固定到堆上,此时该值会有一个稳定的内存地址,它指向的堆中的值在 Pin
后是无法被移动的。 当固定类型 T: !Unpin
时,需要保证数据从被固定到被 drop 这段时期内,其内存不会变得非法或者被重用。
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
async生命周期
async fn
函数如果拥有引用类型的参数,那它返回的 Future
就必须继续等待( .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 {
let x = 5;
borrow_x(&x).await
}
}
async fn borrow_x(x: &u8) -> u8 { *x }
注释代码段报错的原因是因为 x
的生命周期只到 bad
函数的结尾。 但是 Future
显然会活得更久,理解为被调用的async代码段并没有被真正执行,即返回的Future已经超出了bad函数。引用的x就失效了,解决方法就是在async
语句块里调用.await来运行Future,并返回u8类型的值。
async
允许使用 move
关键字来将环境中变量的所有权转移到语句块内。在 .await
时使用普通的锁(如Mutex)也是不安全的,它可能会导致线程池被锁,最终陷入死锁中。需要使用 futures
包下的锁 futures::lock
来替代 Mutex
完成任务。Stream
特征在完成前可以生成多个值。
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>>;
}
我们可以使用 map
,filter
,fold
方法迭代一个 Stream
。但无法使用for
循环,但可以使用while let格式循坏的
,同时还可以使用next
和 try_next
方法:
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
}
join!和 select!
.wait是有顺序地完成任务,但如果要同时运行多个任务,就需要join!宏了。它允许我们同时等待多个不同 Future
的完成,且可以并发地运行这些 Future
。同时 join!
会返回一个元组,里面的值是对应的 Future
执行结束后输出的值。如果希望同时运行一个数组里的多个异步任务,可以使用 futures::future::join_all
方法。如果希望在某一个 Future
报错后就立即停止所有 Future
的执行,可以使用 try_join!。但传给
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)
}
select!
join!
只有等所有 Future
结束后,才能集中处理结果,如果想同时等待多个 Future
,且任何一个 Future
结束后可以立即被处理,可以使用 futures::select!
select!
还支持 default
和 complete
分支:
complete
分支当所有的Future
和Stream
完成后才会被执行,它往往配合loop
使用,loop
用于循环完成所有的Future
default
分支,若没有任何Future
或Stream
处于Ready
状态, 则该分支会被立即执行
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
永远不会被执行。这里如果不加loop循环的话,Future会先运行,但无论a_future还是b_future先完成,都会计算对应的total值,然后函数结束且不会等待另一个任务的完成。因此这里为了同时等待多个future需要加上loop循环。
只有实现了 FusedFuture
,select
才能配合 loop
一起使用。当 Future
一旦完成后,那 select
就不能再对其进行轮询使用,再次调用 poll
会直接返回 Poll::Pending
。.fuse()
方法可以让 Future
实现 FusedFuture
特征。
由于 select
不会通过拿走所有权的方式使用 Future
,而是通过可变引用的方式去使用,这样当 select
结束后,该 Future
若没有被完成,它的所有权还可以继续被其它代码使用。因此Future需要
实现Unpin
特征,而 pin_mut!
宏会为 Future
实现 Unpin
特征。综上,FusedFuture
特征、 Unpin
特征是使用 select
所必须的:
Stream
使用的特征是 FusedStream
。 通过 .fuse()
实现了该特征的 Stream
,对其调用 .next()
或 .try_next()
方法可以获取实现了 FusedFuture
特征的Future。
函数Fuse::terminated()
可以用来构建一个空的 Future。
当某个 Future
有多个拷贝都需要同时运行时,可以使用 FuturesUnordered
类型
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"),
}
}
}