Rust异步学习记录

与JS的Promise实现类似,Generator是基础,Rust也是基于Generator原理构建Future,而Generator的本质则是个状态鸡,换言之就算没有语法糖,你也可以通过写个状态鸡来实现这一套。然而这个知识点虽然有助于理解Future,对于应用还是效果有限,毕竟Rust的异步还是绕着futures/tokio/async-std等库。

前言

Rust的异步初学起来还是挺懵的,因为现在属于早期,各种新旧版本的类库都在网上,譬如tokio_threadpool这个库的已经废弃,分离到tokio的其他模块了,然后查文档找不着北;于是学起来就各种用法用法扑面而来呼到脸上,只剩一脸懵逼。
我主要从使用角度说说我的Rust异步体验吧,本人菜鸡,欢迎提意见。

概念

首先是几个概念,Poll,Future,Waker,Executor,async/await,Task,Reactor,Stream,Sink。

Poll & Future

这俩一起说,Poll可以当作一个枚举型,表示当前状态已经完成或者正在处理中;Future是一个trait,要求有一个poll方法,返回Poll类型,表示当前执行状态。可以简单当作下面的样子。

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

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

这里面的wake方法跟Waker类型关联,实际上Future的实现这里是传的封装了Waker实例的Context类型。

Task & Waker & Executor

这仨在我看来是玄学,参考Waker和Executor。把Task当作一个实现/封装了Future的魔法结构吧,futures和tokio中有各自的实现。Waker顾名思义,它的wake方法用于通知Executor当前Task已经完成,而Executor则会按照一定的调度策略来执行Task。

async & await

async和await这两个都是糖,async后面跟fn,或者闭包,或者花括号block,会把后面的内容转换成impl Future<Output=SomeType>。await则是会被转换成对future的执行动作。

Reactor

Tokio的一个基于mio实现的Waker的管理器,大概就是它会不断poll在pending的task,将就绪的task转移到Executor。

Stream & Sink

这俩的关系类同于CycleJS中的Source和Sink;Stream可以当作Iterator的异步版,一个让你一直读数据的来源,而Sink则是一个让你一直写数据的目标。

体验

让我感觉最神奇的是Future的花式转换:

use futures::task::{Context, Poll};
use futures::{executor, future};

fn read_line(_cx: &mut Context<'_>) -> Poll<i32> {
    Poll::Ready(3)
}

fn main() {
    let a = async { 0 };
    let b = future::lazy(|_| 1);
    let c = future::ready(2);
    let d = future::poll_fn(read_line);
    let tuple = future::join4(a, b, c, d);
    let v = executor::block_on(tuple);
    println!("{:?}", v);
}

异步浅谈中有说,不要在异步函数中执行阻塞操作,所有阻塞操作都要替换成对应的async版本,否则task所在的线程就会被阻塞,然后那个线程上的所有task都得等待这个task了。

这里的同步应该强调的是:需要等待的系统调用。

假设使用tokio。thread::sleep使用tokio的time模块,Tcp/Ups/Uds以及io等都替换成tokio的net/io模块,sync中的channel/Mutex/RwLock也都替换成tokio的sync模块。如果用async-std,那也一样,凡是同步的都换成异步。

先到这儿,后面有了感想再继续加。

参考:Rust异步浅谈async-stdasync-bookAreWeAsyncYet