Rust Async Easy

本文深入探讨了Rust中的异步编程模型,特别是async/await语法和Future trait。异步编程允许在少量线程上高效运行并发任务,而async关键字将代码转换为状态机,await确保在阻塞时释放线程资源。Future作为异步计算的核心,通过poll函数推进任务状态,并借助Waker在准备就绪时唤醒。此外,文章还讨论了Pin和Unpin的作用,以及如何使用join和select宏来管理和同步多个异步任务。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先明确什么是异步。它是一种并发编程模型,**允许在少量线程上(没错是线程)运行大量并发任务,同时通过async/await语法保留普通同步编程的大部分外观与感觉。

异步编程允许适用于像Rust这样的低级语言的高性能实现,同时尽量不改变线程与携程提供便利的可读性。

异步Hello World
use futures::join;
use futures::executor::block_on;

async fn hello_world(){
    println!("Hello world");
}

async fn print_some(){
    hello_world().await;
    println!("Yes I print Hello world");
}

async fn world(){
    println!("World");
}
async fn print_concurrency(){
    let p = print_some();
    let q = world();
    join!(p,q);
}

fn main() {
    block_on(print_concurrency());
}

上面的代码甚至看着没有任何意义,因为没有异步阻塞点,没有并行,只是单纯的Hello world。但是此段代码仍有我们需要关心的东西:

  • async: async将一段代码转换为一个状态机.我们无法像调用函数一样调用状态机,这个状态机调用会返回名为Future的trait
  • 我们在print_some的地方使用的是Join而不是block_on,否则线程在运行时将无法执行其他任何操作。await保证了未来我们如果阻塞,允许其他任务接管当前线程
Future性质与底层

Future trait是Rust异步编程内的核心,可以产生一个值的异步计算,简单源码如下:

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

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

future通过调用poll函数推进,当future完成时,会返回Poll::Ready(result). 如果未完成将会返回Poll:Pending,并咋日后由wake()重新唤醒(没错,就是参数的wake)。当被wake调用后,驱动程序会再次调用poll一边异步任务由更多的进展。

比如我们的Socket编程的listener里面就用到了future

// 这是tcp_listener read的小部分源码,这里用到了Pin是我们后面会说到的东西
impl<R: AsyncRead + ?Sized + Unpin> Future for Read<'_, R> {
    type Output = io::Result<usize>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let this = &mut *self;
        Pin::new(&mut this.reader).poll_read(cx, this.buf)
    }
}

官网也有给出我们去模拟Socket listen时候的简单化编程:

// 注,这一段是伪代码,定法运行
pub struct SocketRead<'a> {
    socket: &'a Socket,
}

impl SimpleFuture for SocketRead<'_> {
    type Output = Vec<u8>;

    fn poll(&mut self, wake: fn()) -> Poll<Self::Output> {
        if self.socket.has_data_to_read() {
            // The socket has data -- read it into a buffer and return it.
            Poll::Ready(self.socket.read_buf())
        } else {
            // The socket does not yet have data.
            //
            // Arrange for `wake` to be called once data is available.
            // When data becomes available, `wake` will be called, and the
            // user of this `Future` will know to call `poll` again and
            // receive data.
            self.socket.set_readable_callback(wake);
            Poll::Pending
        }
    }
}

我们注意,poll更多的作用类似于标志位,主要是反应现在再跑的任务到底跑完没有。真正轮询还得靠wake.wake虽然是第二个参数,也有那么一丝不起眼,但是还算特殊future 需要确保在准备好取得更多进展后再次对其进行轮询。这是通过Waker 类型完成的。

注意,Waker本身就是一种类型,通过导入std::task::导入Waker,关于其源码可自行翻阅。其结构提本身包括数据和方法指针两种结构。

关于wake函数我会在后面补全

async/.await关键字

这两个关键词是Rust语法的特殊部分, 可以让出对当前线程的控制而不是阻塞,从而允其他代码在等待操作完成时取得进展。

比如最开始的例子,或者我如下的例子:

async fn foo() -> u8 { 5 }

fn bar() -> impl Future<Output = u8> {
    async {
        let x: u8 = foo().await;
        x + 5
    }
}

我们需要用.await关键字使惰性的async函数运行得到结果。如果这个时候foo被阻塞,将让出对线程的控制,直到产生进展

经过我的疯狂测试,本应该join发生并行的地方却不断串行,这跟我们await的对象有关。对于我们自定义的await对象想要其并行运行,或者说让开所有权,需要如下面的例子一样:

use futures;
use futures::executor::block_on;
use tokio::runtime::Runtime;

async fn wait(){
    for i in 1..10{}
    tokio::time::delay_for(tokio::time::Duration::from_secs(0)).await;
}

async fn test1(){
    wait().await;
    println!("test1");
    println!("test1_done");
}

async fn test2(){
    println!("test2");
    wait().await;
    println!("test2_done");
}

async fn main_(){
    let a = test1();
    let b = test2();
    futures::join!(a,b);
}


fn main() {
    let mut runtime = Runtime::new().unwrap();
    runtime.block_on(main_());
}
test2
test1
test1_done
test2_done

不过这个异步终究是在一个线程中完成的!言归正传,async块拥有闭包的操作

多线程要考虑的问题更加麻烦,因为可能会牵扯带Future在线程间移动(明白Python为啥有GIL了吧)所以我么需要考虑Send Arc等等方式手动保证线程移动的安全性

Pin 固定

Pin于Unpin标记一起工作, 实现了!Unpin的对象永远不会被移动。意思就是,**异步的东西为了防止空引用需要使用Pin将其固定在一定的内存中。**Pin类型会包裹指针类型,保证指针指向的值不被移动,eg. Pin<&mut T>, Pin<&T>等等。

换句话说,Pin包裹的指针对应的数据无法移动,除非Unpin解封

大部分类型都以用Pin包裹指针。

总之Pin比较难,直接看官方文档吧,我在这里说不清楚的:https://rust-lang.github.io/async-book/04_pinning/01_chapter.html

Select 执行

Join我们之前已经用过了,这里再简单说一下,Join是执行所有异步任务并等待完成的宏) 对于返回Result的future, 更考虑使用try_join!:

  • 子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)
}

select!宏同样可同时运行多个future,而且**允许用户再任意一个future完成时进行响应

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

async fn task_one() {tokio::time::delay_for(tokio::time::Duration::from_secs(1)).await;println!("This is task_one");}
async fn task_two() {println!("This is task_two");}

async fn race_tasks() {
    // 这里fuse是因为select的future必须实现Unpin和FusedFuture这两个trait, 下同
    let t1 = task_one().fuse();
    let t2 = task_two().fuse();

    pin_mut!(t1, t2);

    select! {
   		// 这个宏是不是有点枚举类型的样子
        () = t1 => println!("task one completed first"),
        () = t2 => println!("task two completed first"),
    }
}

fn main() {
    let mut runtime = Runtime::new().unwrap();
    runtime.block_on(race_tasks());
}
This is task_two
task two completed first

我们看到select模块有点像枚举模块,**其本身也孕育我们定义分支defaultcomplete:

  • default:如果选中的future尚未完成,就会调用default.拥有defaut的select总是会立即返回
  • complete: 用于所有选中future都已经完成的例子

关于select!所需要的trait:

  • Pin: Select是按照指针对应的地址去调用函数的,所以用Pin还是很正常的
  • fuesd 这个标志的作用是在future完成后select不可以对它使用poll(就是完成之后直接把它踢出任务队列)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值