您混淆了一些概念。
Concurrency is not parallelism、async 和 await 是并发的工具,这有时可能意味着它们也是并行工具。
此外,future 是否立即被轮询与选择的语法是正交的。
async / await
关键字async 和await 的存在使创建异步代码并与之交互更容易阅读,并且看起来更像“普通”同步代码。据我所知,在所有具有此类关键字的语言中都是如此。
更简单的代码
这段代码创建了一个在轮询时将两个数字相加的未来
之前
fn long_running_operation(a: u8, b: u8) -> impl Future<Output = u8> {
struct Value(u8, u8);
impl Future for Value {
type Output = u8;
fn poll(self: Pin<&mut Self>, _ctx: &mut Context) -> Poll<Self::Output> {
Poll::Ready(self.0 + self.1)
}
}
Value(a, b)
}
之后
async fn long_running_operation(a: u8, b: u8) -> u8 {
a + b
}
请注意,“之前”代码基本上是implementation of today's poll_fn function
另请参阅Peter Hall's answer,了解如何更好地跟踪许多变量。
参考文献
async/await 的一个潜在令人惊讶的事情是它启用了一种以前不可能的特定模式:在期货中使用引用。下面是一些以异步方式用值填充缓冲区的代码:
之前
use std::io;
fn fill_up<'a>(buf: &'a mut [u8]) -> impl Future<Output = io::Result<usize>> + 'a {
futures::future::lazy(move |_| {
for b in buf.iter_mut() { *b = 42 }
Ok(buf.len())
})
}
fn foo() -> impl Future<Output = Vec<u8>> {
let mut data = vec![0; 8];
fill_up(&mut data).map(|_| data)
}
编译失败:
error[E0597]: `data` does not live long enough
--> src/main.rs:33:17
|
33 | fill_up_old(&mut data).map(|_| data)
| ^^^^^^^^^ borrowed value does not live long enough
34 | }
| - `data` dropped here while still borrowed
|
= note: borrowed value must be valid for the static lifetime...
error[E0505]: cannot move out of `data` because it is borrowed
--> src/main.rs:33:32
|
33 | fill_up_old(&mut data).map(|_| data)
| --------- ^^^ ---- move occurs due to use in closure
| | |
| | move out of `data` occurs here
| borrow of `data` occurs here
|
= note: borrowed value must be valid for the static lifetime...
之后
use std::io;
async fn fill_up(buf: &mut [u8]) -> io::Result<usize> {
for b in buf.iter_mut() { *b = 42 }
Ok(buf.len())
}
async fn foo() -> Vec<u8> {
let mut data = vec![0; 8];
fill_up(&mut data).await.expect("IO failed");
data
}
这行得通!
调用async 函数不会运行任何东西
另一方面,Future 和围绕期货的整个系统的实现和设计与关键字async 和await 无关。事实上,在 async / await 关键字出现之前,Rust 就有一个蓬勃发展的异步生态系统(例如 Tokio)。 JavaScript 也是如此。
为什么Futures 在创建时不立即轮询?
要获得最权威的答案,请查看 RFC 拉取请求中的 this comment from withoutboats:
Rust 的期货与其他期货的根本区别
语言是 Rust 的未来不会做任何事情,除非被轮询。这
整个系统都是围绕这个构建的:例如,取消是
正是因为这个原因,放弃了未来。相比之下,在其他
语言,调用 async fn 会启动一个开始执行的未来
马上。
关于这一点的一点是,Rust 中的 async 和 await 并不是天生的
并发构造。如果你的程序只使用 async &
await 并且没有并发原语,您程序中的代码将
以定义的、静态已知的线性顺序执行。显然,大多数
程序将使用某种并发来调度多个,
事件循环上的并发任务,但他们不必这样做。这是什么
意味着您可以-琐碎地-在本地保证订购
某些事件,即使在两者之间执行了非阻塞 IO
他们希望与更大的非本地集合异步
事件(例如,您可以严格控制 a 内事件的顺序
请求处理程序,同时与许多其他请求并发
处理程序,甚至在等待点的两侧)。
这个属性为 Rust 的 async/await 语法提供了一种本地的
推理和低级控制使 Rust 成为现在的样子。跑起来
到第一个等待点不会本质上违反这一点 - 你会
仍然知道代码何时执行,它只会分两步执行
不同的地方取决于它是在一个之前还是之后
等待。但是,我认为其他语言做出的决定开始
立即执行很大程度上源于他们的系统
调用 async fn 时立即同时安排任务
(例如,这是我得到的潜在问题的印象
来自 Dart 2.0 文档)。
this discussion from munificent 涵盖了 Dart 2.0 的一些背景:
大家好,我在 Dart 团队。 Dart 的 async/await 主要是由
Erik Meijer,他也从事 C# 的 async/await 工作。在 C# 中,异步/等待
与第一个等待同步。对于 Dart,Erik 和其他人认为
C# 的模型太混乱了,而是指定了 async
函数在执行任何代码之前总是产生一次。
当时,我和我团队中的另一个人的任务是成为
豚鼠尝试我们的新的进行中的语法和语义
包管理器。基于那个经验,我们觉得异步函数
应该与第一个等待同步运行。我们的论点是
主要是:
总是让步一次会无缘无故地降低性能。在大多数情况下,这无关紧要,但在某些情况下确实如此
做。即使在你可以忍受它的情况下,流血也是一种拖累
无处不在的小性能。
-
总是让步意味着某些模式无法使用 async/await 实现。特别是,有这样的代码真的很常见
(这里是伪代码):
getThingFromNetwork():
if (downloadAlreadyInProgress):
return cachedFuture
cachedFuture = startDownload()
return cachedFuture
换句话说,您有一个异步操作,您可以在它完成之前多次调用它。以后的调用使用相同的
先前创建的未决未来。你想确保你不开始
多次操作。这意味着您需要同步
在开始操作之前检查缓存。
如果 async 函数从一开始就是异步的,那么上面的函数就不能使用 async/await。
我们为我们的案子辩护,但最终语言设计者坚持
从顶部异步。这是几年前的事了。
原来是打错电话了。性能成本是真实的
足以让许多用户产生“异步函数是
慢”并开始避免使用它,即使在性能命中的情况下
是负担得起的。更糟糕的是,我们看到令人讨厌的并发错误
认为他们可以在函数的顶部做一些同步工作,并且
沮丧地发现他们创造了竞争条件。总的来说,它
似乎用户不会自然地假设 async 函数之前会产生
执行任何代码。
因此,对于 Dart 2,我们现在将非常痛苦的突破性更改
将异步函数更改为与第一个等待同步并
通过该过渡迁移我们所有现有的代码。我很高兴
我们正在做出改变,但我真的希望我们做了正确的事
第一天。
我不知道 Rust 的所有权和性能模型是否有所不同
对你的限制,从顶部异步确实更好,
但根据我们的经验,sync-to-the-first-await 显然更好
Dart 的权衡。
cramert replies(注意其中一些语法现在已经过时了):
如果您需要在调用函数时立即执行代码
而不是稍后轮询未来时,您可以编写您的
函数如下:
fn foo() -> impl Future<Item=Thing> {
println!("prints immediately");
async_block! {
println!("prints when the future is first polled");
await!(bar());
await!(baz())
}
}
代码示例
这些示例使用 Rust 1.39 和 futures crate 0.3.1 中的异步支持。
C# 代码的文字转录
use futures; // 0.3.1
async fn long_running_operation(a: u8, b: u8) -> u8 {
println!("long_running_operation");
a + b
}
fn another_operation(c: u8, d: u8) -> u8 {
println!("another_operation");
c * d
}
async fn foo() -> u8 {
println!("foo");
let sum = long_running_operation(1, 2);
another_operation(3, 4);
sum.await
}
fn main() {
let task = foo();
futures::executor::block_on(async {
let v = task.await;
println!("Result: {}", v);
});
}
如果你调用foo,Rust 中的事件顺序将是:
- 返回实现
Future<Output = u8> 的内容。
就是这样。尚未完成“实际”工作。如果您获取foo 的结果并推动它完成(通过轮询,在本例中是通过futures::executor::block_on),那么接下来的步骤是:
调用long_running_operation 返回了实现Future<Output = u8> 的东西(它还没有开始工作)。
another_operation 确实有效,因为它是同步的。
.await 语法导致long_running_operation 中的代码启动。 foo 未来将继续返回“未准备好”,直到计算完成。
输出将是:
foo
another_operation
long_running_operation
Result: 3
注意这里没有线程池:这都是在一个线程上完成的。
async 块
您也可以使用async 块:
use futures::{future, FutureExt}; // 0.3.1
fn long_running_operation(a: u8, b: u8) -> u8 {
println!("long_running_operation");
a + b
}
fn another_operation(c: u8, d: u8) -> u8 {
println!("another_operation");
c * d
}
async fn foo() -> u8 {
println!("foo");
let sum = async { long_running_operation(1, 2) };
let oth = async { another_operation(3, 4) };
let both = future::join(sum, oth).map(|(sum, _)| sum);
both.await
}
这里我们将同步代码包装在 async 块中,然后等待两个操作完成,然后此函数才会完成。
请注意,像这样包装同步代码不是对于实际需要很长时间的任何事情都是一个好主意;请参阅What is the best approach to encapsulate blocking I/O in future-rs? 了解更多信息。
使用线程池
// Requires the `thread-pool` feature to be enabled
use futures::{executor::ThreadPool, future, task::SpawnExt, FutureExt};
async fn foo(pool: &mut ThreadPool) -> u8 {
println!("foo");
let sum = pool
.spawn_with_handle(async { long_running_operation(1, 2) })
.unwrap();
let oth = pool
.spawn_with_handle(async { another_operation(3, 4) })
.unwrap();
let both = future::join(sum, oth).map(|(sum, _)| sum);
both.await
}