首先明确什么是异步。它是一种并发编程模型,**允许在少量线程上(没错是线程)运行大量并发任务,同时通过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模块有点像枚举模块,**其本身也孕育我们定义分支default
与complete
:
- default:如果选中的future尚未完成,就会调用default.拥有defaut的select总是会立即返回
- complete: 用于所有选中future都已经完成的例子
关于select!所需要的trait:
- Pin: Select是按照指针对应的地址去调用函数的,所以用Pin还是很正常的
- fuesd 这个标志的作用是在future完成后select不可以对它使用poll(就是完成之后直接把它踢出任务队列)