本文翻译自Rust futures: an uneducated, short and hopefully not boring tutorial-part1
介绍
既然能够看到这个页面,想必各位看官至少应该是一个程序员,并且很有可能是中意这门新语言的。如果各位看官还有爱逛论坛或者社区的习惯,或多或少可能看到过关于Future的讨论。大量优秀的项目已经改由Future重构底层实现,比如Hyper,并且这种转变正在成为趋势,如果对rust感兴趣,Future是一个不容错过的特性。尽管受到大量的关注,但不能否认的是,Future是一个很难理解的概念,如果各位看官对自己的英文水平与rust基础很自信,可以直接看官方的Crichton's tutorial,但是对于像我这样的不太自信的选手,我认为官方文档并不适合作为学习教材。
我猜想我可能并不是唯一的一个不太自信的选手,所以我把我的一些思考和实践记录下来,希望这个文档能帮到大家。
概要
Future的本质特征就是其字面意思,指一系列不会立即执行,但是可能在未来某个时间执行的函数。标准函数一旦被调用将会立即执行,为什么要引入Future这样的概念呢?原因很多,包括性能,架构的优雅性,可扩展性等。Future的缺点也十分明显,即很难用代码实现。通常在一个线程内,函数是串行执行的,但是当不确定一个函数会在什么时候执行时,就需要精巧的设计这些调用之间的因果关系,否则,程序的执行流程就会乱套了。
由于执行时间的不确定带来了极大的编程难度,rust语言本身增加了大量的辅助机制来帮助并不精通此道的程序员们使用这个特性。
Rust Future
Rust的Future总是用Result来表达,也就是说,需要显式指定返回值类型以及错误类型。
我们先定义一个普通函数,然后把它转换为Future。简单示例如下所示,这个示例返回一个u32或者是被Box包裹的Error trait:
fn my_fn() -> Result<u32, Box<Error>> {
Ok(100)
}
这个函数转换为Future如下所示:
fn my_fut() -> impl Future<Item = u32, Error = Box<Error>> {
ok(100)
}
这两段代码有两处不同:
1 返回值不再是Result,而是一个impl Future,这样的标记允许我们返回一个Future。
2 返回参数列表为<Item = u32, Error = Box<Error>>,精确描述了返回值与错误类型。
两个函数的返回值是不一样的,在普通函数中,用Ok(100)返回,这种写法使用到了Ok枚举类型,而在Future函数中,使用的是小写的ok方法。
函数改造完成后,要怎样执行呢?标准函数被调用后直接执行,需要注意的是,函数返回值是一个Result,必须要用unwarp取其内部包裹的值。
let retval = my_fn().unwrap();
println!("{:?}", retval);
由于Future调用在实际的执行之前返回,更准确的说,返回的是一段在将来执行的代码。为了能够在将来执行,需要额外引入一种途径,我们通过调用reactor的run方法来执行Future。
let mut reactor = Core::new().unwrap();
let retval = reactor.run(my_fut()).unwrap();
println!("{:?}", retval);
注意返回值同样需要unwrap。
这样看起来,Future并没有想象中难,我们继续。
级联
级联是Future最重要的特性之一。考虑一个很典型的场景:假设你向你的父母发email,通知他们一起吃晚饭,收到确认后,你会开始准备晚餐,或者直接被拒绝。这些事件之间是有依赖关系的的,只有当前一个事件发生了,后一个事件才会启动。这一系列有因果关系的事件就是级联的概念,我们来看一个例子。
首先定义一个普通函数与一个Future。
fn my_fn_squared(i: u32) -> Result<u32, Box<Error>> {
Ok(i * i)
}
fn my_fut_squared(i: u32) -> impl Future<Item = u32, Error = Box<Error>> {
ok(i * i)
}
直接调用普通函数:
let retval = my_fn().unwrap();
println!("{:?}", retval);
let retval2 = my_fn_squared(retval).unwrap();
println!("{:?}", retval2);
我们可以使用reactor来执行同样的代码:
let mut reactor = Core::new().unwrap();
let retval = reactor.run(my_fut()).unwrap();
println!("{:?}", retval);
let retval2 = reactor.run(my_fut_squared(retval)).unwrap();
println!("{:?}", retval2);
但是对于Future而言,可以有更好的办法。由于Future是trait,有一个and_then内建方法,使用这个方法我们可以实现相同的语义,但是不用显式的创建以及调用两次reactor run。
let chained_future = my_fut().and_then(|retval| my_fn_squared(retval));
let retval2 = reactor.run(chained_future).unwrap();
println!("{:?}", retval2);
看第一行,我们创建了一个Future,叫做chained_future,同时包含了my_fut与my_fn_squared。
这里有一个抖机灵的点,就是如何在不同的Future之间传递结果,rust通过闭包捕获环境变量的方式传递Future返回的结果。
执行过程如下:
1 调度并且执行my_fut()
2 当my_fut()执行完成,创建一个叫做retval的变量,并将my_fut()返回的结果保存至retval
3 将retval作为my_fn_squared(i: u32)的参数传递进去,调度并执行
4 把上述操作过程打包成一个名为chained_future的调用链,chained_future也是一个Future。
第二行与上文类似,调用了reactor.run来执行Future并返回结果。
通过这样的方式可以把无限个Future打包成一个调用链,这样的打包方式不会造成额外的开销,所以不需要担心性能问题。
rust的 borrow checked
特性可能会Future chain写起来不是那么轻松,这种情况下可以尝试在闭包参数中添加move
指令。
Future与普通函数混用
除了可以将Future级联之外,还可以将普通函数级联至Future。这个特性很有用,因为并不是所有函数都需要使用到Future特性。另一方面,也会出现在Future中调用外部函数的情况。如果这个函数返回类型不是Result,可以将这个函数简单的添加在一个闭包中。例如,假如有一个普通函数:
fn fn_plain(i: u32) -> u32 {
i - 50
}
级联后的Future如下所示:
let chained_future = my_fut().and_then(|retval| {
let retval2 = fn_plain(retval);
my_fut_squared(retval2)
});
let retval3 = reactor.run(chained_future).unwrap();
println!("{:?}", retval3);
如果函数返回Result
,则有更好的办法。我们来尝试将my_fn_squared(i: u32) -> Result<u32, Box<Error>
方法打包进Future chain。
在这里由于返回值是Result
所以无法调用and_then
, 但是Future有一个方法done()
可以将Result
转换为impl Future
.这意味着我们可以将普通的函数通过done
方法把它包装成一个Future。
let chained_future = my_fut().and_then(|retval| {
done(my_fn_squared(retval)).and_then(|retval2| my_fut_squared(retval2))
});
let retval3 = reactor.run(chained_future).unwrap();
println!("{:?}", retval3);
注意第二行:done(my_fn_squared(retval))
,能够这样写的原因是,我们将普通函数通过done
方法转换成一个impl Future
。
现在我们不使用done
方法试试:
let chained_future = my_fut().and_then(|retval| {
my_fn_squared(retval).and_then(|retval2| my_fut_squared(retval2))
});
let retval3 = reactor.run(chained_future).unwrap();
println!("{:?}", retval3);
编译不通过!
Compiling tst_fut2 v0.1.0 (file:///home/MINDFLAVOR/mindflavor/src/rust/tst_future_2)
error[E0308]: mismatched types
--> src/main.rs:136:50
|
136 | my_fn_squared(retval).and_then(|retval2| my_fut_squared(retval2))
| ^^^^^^^^^^^^^^^^^^^^^^^ expected enum `std::result::Result`, found anonymized type
|
= note: expected type `std::result::Result<_, std::boxed::Box<std::error::Error>>`
found type `impl futures::Future`
error: aborting due to previous error
error: Could not compile `tst_fut2`.
expected type std::result::Result<_, std::boxed::Box<std::error::Error>> found type impl futures::Future
,这个错误有点让人困惑。我们期望通过传递一个impl Future,但是我们用Result代替了。我们将会在第二部分讨论它。
范型
最后但并非最不重要的,Future与范型一起工作不需要借助于任何黑魔法。
看一个例子:
fn fut_generic_own<A>(a1: A, a2: A) -> impl Future<Item = A, Error = Box<Error>>
where
A: std::cmp::PartialOrd,
{
if a1 < a2 {
ok(a1)
} else {
ok(a2)
}
}
这个函数返回的是 a1 与 a2之间的较小的值,但是即便我们很确定这个函数没有错误也需要给出Error
,此外,返回值在这种情况下是小写的ok
(原因是因为ok是函数, 而不是enmu
)
现在我们执行这个Future:
let future = fut_generic_own("Sampdoria", "Juventus");
let retval = reactor.run(future).unwrap();
println!("fut_generic_own == {}", retval);
阅读到现在,你可能对Future应该有所了解了,在这边文章里,你可能注意到我没有使用&
, 并且仅使用函数自身的值。这是因为使用impl Future
后生命周期的行为会有差异,我将在下一篇文章中解释如何使用它们。在下一篇文章中我们还会讨论如何在Future chain中处理错误和使用await!()宏。
网友评论