异步并发编程

常见的并发模型

  • 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>>;
}

我们可以使用 mapfilterfold 方法迭代一个 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循环。

只有实现了 FusedFutureselect 才能配合 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"),
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值