常见的并发模型
- 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用于循环完成所有的Futuredefault分支,若没有任何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"),
}
}
}

被折叠的 条评论
为什么被折叠?



