你将几个概念混淆了。
并发不等于并行, 而
async
和
await
是处理
并发的工具,这意味着它们有时也是处理并行的工具。
此外,一个Future是否立即被轮询与所选择的语法无关。
async
/await
async
和
await
关键字的存在是为了使创建和交互异步代码更容易阅读,并且看起来更像"正常"同步代码。据我所知,在所有拥有这些关键字的语言中都是如此。
更简单的代码
下面是一个创建Future对象,在轮询时将两个数字相加的代码:
之前的代码
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
}
请注意,“before”代码基本上是今天的
poll_fn
函数的实现。
另请参见
Peter Hall的答案,了解如何更好地跟踪多个变量。
参考资料
async
/await
可能会让人惊讶的一点是,它可以实现一种以前无法实现的特定模式:在futures中使用引用。以下是一些以异步方式填充缓冲区的代码:
之前
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也是如此。
为什么在创建时不立即轮询Future
?
对于最权威的答案,请查看withoutboats的此评论:
Rust的futures与其他语言的futures的一个基本区别在于,除非被轮询,否则Rust的futures不会执行任何操作。整个系统都是围绕这一点构建的:例如,取消操作正是因为这个原因而放弃future。相比之下,在其他语言中,调用async fn会启动一个立即开始执行的future。
关于这一点的一个要点是,Rust中的async & await并不是本质上的并发构造。如果你的程序只使用async & await而没有并发原语,那么你程序中的代码将按照定义好的、静态已知的线性顺序执行。显然,大多数程序都会使用某种并发方式在事件循环上安排多个并发任务,但它们不必这样做。这意味着,即使在它们之间执行了一些你希望与某个更大的非本地事件集合异步执行的非阻塞IO(例如,在一个请求处理程序内部严格控制事件的顺序),你也可以轻松地本地保证某些事件的顺序。
这种特性赋予了Rust的async/await语法一种本地推理和低级控制的能力,这正是Rust成为它所是的原因。到达第一个await点并不会本质上违反这一点——你仍然会知道代码何时执行,只是它会在await前后两个不同的位置执行。然而,我认为其他语言开始立即执行的决定很大程度上源于它们的系统,在调用async fn时立即并发地安排一个任务(例如,这是我从Dart 2.0文档中获得的底层问题的印象)。
一些Dart 2.0的背景在这篇来自Munificent的讨论中有所涉及:
Hi, I'm on the Dart team. Dart's async/await was designed mainly by
Erik Meijer, who also worked on async/await for C#. In C#, async/await
is synchronous to the first await. For Dart, Erik and others felt that
C#'s model was too confusing and instead specified that an async
function always yields once before executing any code.
At the time, I and another on my team were tasked with being the
guinea pigs to try out the new in-progress syntax and semantics in our
package manager. Based on that experience, we felt async functions
should run synchronously to the first await. Our arguments were
mostly:
Always yielding once incurs a performance penalty for no good reason. In most cases, this doesn't matter, but in some it really
does. Even in cases where you can live with it, it's a drag to bleed a
little perf everywhere.
Always yielding means certain patterns cannot be implemented using async/await. In particular, it's really common to have code like
(pseudo-code here):
getThingFromNetwork():
if (downloadAlreadyInProgress):
return cachedFuture
cachedFuture = startDownload()
return cachedFuture
In other words, you have an async operation that you can call multiple times before it completes. Later calls use the same
previously-created pending future. You want to ensure you don't start
the operation multiple times. That means you need to synchronously
check the cache before starting the operation.
If async functions are async from the start, the above function can't use async/await.
We pleaded our case, but ultimately the language designers stuck with
async-from-the-top. This was several years ago.
That turned out to be the wrong call. The performance cost is real
enough that many users developed a mindset that "async functions are
slow" and started avoiding using it even in cases where the perf hit
was affordable. Worse, we see nasty concurrency bugs where people
think they can do some synchronous work at the top of a function and
are dismayed to discover they've created race conditions. Overall, it
seems users do not naturally assume an async function yields before
executing any code.
So, for Dart 2, we are now taking the very painful breaking change to
change async functions to be synchronous to the first await and
migrating all of our existing code through that transition. I'm glad
we're making the change, but I really wish we'd done the right thing
on day one.
I don't know if Rust's ownership and performance model place different
constraints on you where being async from the top really is better,
but from our experience, sync-to-the-first-await is clearly the better
trade-off for Dart.
克拉默特回复(请注意,现在有些语法已经过时):
If you need code to execute immediately when a function is called
rather than later on when the future is polled, you can write your
function like this:
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;
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 中的事件顺序将是:
1. 返回实现了
Future<Output = u8>
的内容。
就这样,还没有进行“实际”的工作。 如果您获取
foo
的结果并将其驱动至完成状态(通过轮询它,在这种情况下为
futures :: executor :: block_on
),那么下一步是:
2. 从调用
long_running_operation
返回实现了
Future<Output = u8>
的内容(尚未开始工作)。
3.
another_operation
作为同步操作运行。
4.
.await
语法使
long_running_operation
中的代码开始运行。
foo
future将继续返回“未准备就绪”,直到计算完成。
输出将是:
foo
another_operation
long_running_operation
Result: 3
请注意,这里没有线程池:所有操作都在单个线程上执行。
async
块
你也可以使用
async
块:
use futures::{future, FutureExt};
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
块中,然后等待两个操作都完成后,该函数才会完成。
请注意,像这样包装同步代码对于实际上需要很长时间的任何事情来说都不是一个好主意;有关更多信息,请参见什么是最好的方法来封装future-rs中的阻塞I/O?。
使用线程池
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
}