async/await 在 Rust 中的用途是什么?
What is the purpose of async/await in Rust?
在像 C# 这样的语言中,给出这段代码(我不是故意使用 await
关键字):
async Task Foo()
{
var task = LongRunningOperationAsync();
// Some other non-related operation
AnotherOperation();
result = task.Result;
}
第一行,长操作在另一个线程运行,返回一个Task
(即future)。然后,您可以执行另一个与第一个操作并行 运行 的操作,最后,您可以等待操作完成。我认为这也是 async
/await
在 Python、JavaScript 等
中的行为
另一方面,在 Rust 中,我在 the RFC 中读到:
A fundamental difference between Rust's futures and those from other languages is that Rust's futures do not do anything unless polled. The whole system is built around this: for example, cancellation is dropping the future for precisely this reason. In contrast, in other languages, calling an async fn spins up a future that starts executing immediately.
在这种情况下,Rust 中 async
/await
的目的是什么?在其他语言中,这种表示法是 运行 并行操作的便捷方式,但如果 async
函数的调用没有 运行 任何东西,我看不出它在 Rust 中是如何工作的。
你混淆了一些概念。
Concurrency is not parallelism,async
和 await
是 并发的工具 ,这有时可能意味着它们也是并行的工具。
此外,是否立即轮询未来与所选语法正交。
async
/ await
关键字 async
和 await
的存在是为了让异步代码的创建和交互更容易阅读,并且看起来更像 "normal" 同步代码。据我所知,所有具有此类关键字的语言都是如此。
更简单的代码
这是创建未来的代码,在轮询时将两个数字相加
之前
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" 代码基本上是 implementation of today's poll_fn
function
另请参阅了解如何更好地跟踪许多变量。
参考资料
关于 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.
也是如此
为什么 Future
没有在创建时立即进行轮询?
要获得最权威的答案,请查看 RFC 拉取请求中的 this comment from withoutboats:
A fundamental difference between Rust's futures and those from other
languages is that Rust's futures do not do anything unless polled. The
whole system is built around this: for example, cancellation is
dropping the future for precisely this reason. In contrast, in other
languages, calling an async fn spins up a future that starts executing
immediately.
A point about this is that async & await in Rust are not inherently
concurrent constructions. If you have a program that only uses async &
await and no concurrency primitives, the code in your program will
execute in a defined, statically known, linear order. Obviously, most
programs will use some kind of concurrency to schedule multiple,
concurrent tasks on the event loop, but they don't have to. What this
means is that you can - trivially - locally guarantee the ordering of
certain events, even if there is nonblocking IO performed in between
them that you want to be asynchronous with some larger set of nonlocal
events (e.g. you can strictly control ordering of events inside of a
request handler, while being concurrent with many other request
handlers, even on two sides of an await point).
This property gives Rust's async/await syntax the kind of local
reasoning & low-level control that makes Rust what it is. Running up
to the first await point would not inherently violate that - you'd
still know when the code executed, it would just execute in two
different places depending on whether it came before or after an
await. However, I think the decision made by other languages to start
executing immediately largely stems from their systems which
immediately schedule a task concurrently when you call an async fn
(for example, that's the impression of the underlying problem I got
from the Dart 2.0 document).
Dart 2.0 的一些背景由 this discussion from 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.
cramert replies(注意有些语法现在已经过时了):
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; // 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>
的东西是 returned。
就是这样。 "actual" 工作尚未完成。如果您获取 foo
的结果并将其推向完成(通过轮询,在本例中是通过 futures::executor::block_on
),那么接下来的步骤是:
实现 Future<Output = u8>
的东西是 return 通过调用 long_running_operation
编辑的(它还没有开始工作)。
another_operation
确实有效,因为它是同步的。
.await
语法导致 long_running_operation
中的代码开始。 foo
未来将继续 return "not ready" 直到计算完成。
输出将是:
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
块中,然后等待两个操作完成,然后此功能才会完成。
请注意,像这样包装同步代码不是对于实际需要很长时间的任何事情的好主意;有关详细信息,请参阅 。
有线程池
// 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
}
考虑这个简单的伪JavaScript代码获取一些数据,处理它,根据上一步获取更多数据,总结它,然后打印结果:
getData(url)
.then(response -> parseObjects(response.data))
.then(data -> findAll(data, 'foo'))
.then(foos -> getWikipediaPagesFor(foos))
.then(sumPages)
.then(sum -> console.log("sum is: ", sum));
以async/await
形式,即:
async {
let response = await getData(url);
let objects = parseObjects(response.data);
let foos = findAll(objects, 'foo');
let pages = await getWikipediaPagesFor(foos);
let sum = sumPages(pages);
console.log("sum is: ", sum);
}
它引入了很多一次性变量,可以说比带有 promise 的原始版本差。那么何必呢?
考虑这一变化,稍后在计算中需要变量 response
和 objects
:
async {
let response = await getData(url);
let objects = parseObjects(response.data);
let foos = findAll(objects, 'foo');
let pages = await getWikipediaPagesFor(foos);
let sum = sumPages(pages, objects.length);
console.log("sum is: ", sum, " and status was: ", response.status);
}
并尝试用 promises 以原始形式重写它:
getData(url)
.then(response -> Promise.resolve(parseObjects(response.data))
.then(objects -> Promise.resolve(findAll(objects, 'foo'))
.then(foos -> getWikipediaPagesFor(foos))
.then(pages -> sumPages(pages, objects.length)))
.then(sum -> console.log("sum is: ", sum, " and status was: ", response.status)));
每次需要回溯之前的结果时,都需要将整个结构嵌套得更深一层。这很快就会变得很难阅读和维护,但是 async
/await
版本没有这个问题。
Rust 中 async
/await
的目的是提供并发工具包——与 C# 和其他语言相同。
在 C# 和 JavaScript 中,async
方法立即开始 运行,并且无论您 await
结果与否,它们都会被安排。在 Python 和 Rust 中,当您调用 async
方法时,在您 await
之前什么都不会发生(它甚至没有被安排)。但无论哪种方式,它在很大程度上都是相同的编程风格。
生成另一个任务(与当前任务并发且独立于当前任务运行)的能力由库提供:参见 async_std::task::spawn
and tokio::task::spawn
。
至于为什么 Rust async
不完全像C#,好吧,考虑一下两种语言的区别:
Rust 不鼓励全局可变状态。 在 C# 和 JS 中,每个 async
方法调用都隐式添加到全局可变队列中。这是对某些隐式上下文的副作用。不管是好是坏,这不是 Rust 的风格。
Rust 不是框架。 C# 提供默认事件循环是有道理的。它还提供了一个很棒的垃圾收集器!许多其他语言中的标准内容是 Rust 中的可选库。
在像 C# 这样的语言中,给出这段代码(我不是故意使用 await
关键字):
async Task Foo()
{
var task = LongRunningOperationAsync();
// Some other non-related operation
AnotherOperation();
result = task.Result;
}
第一行,长操作在另一个线程运行,返回一个Task
(即future)。然后,您可以执行另一个与第一个操作并行 运行 的操作,最后,您可以等待操作完成。我认为这也是 async
/await
在 Python、JavaScript 等
另一方面,在 Rust 中,我在 the RFC 中读到:
A fundamental difference between Rust's futures and those from other languages is that Rust's futures do not do anything unless polled. The whole system is built around this: for example, cancellation is dropping the future for precisely this reason. In contrast, in other languages, calling an async fn spins up a future that starts executing immediately.
在这种情况下,Rust 中 async
/await
的目的是什么?在其他语言中,这种表示法是 运行 并行操作的便捷方式,但如果 async
函数的调用没有 运行 任何东西,我看不出它在 Rust 中是如何工作的。
你混淆了一些概念。
Concurrency is not parallelism,async
和 await
是 并发的工具 ,这有时可能意味着它们也是并行的工具。
此外,是否立即轮询未来与所选语法正交。
async
/ await
关键字 async
和 await
的存在是为了让异步代码的创建和交互更容易阅读,并且看起来更像 "normal" 同步代码。据我所知,所有具有此类关键字的语言都是如此。
更简单的代码
这是创建未来的代码,在轮询时将两个数字相加
之前
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" 代码基本上是 implementation of today's poll_fn
function
另请参阅
参考资料
关于 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.
为什么 Future
没有在创建时立即进行轮询?
要获得最权威的答案,请查看 RFC 拉取请求中的 this comment from withoutboats:
A fundamental difference between Rust's futures and those from other languages is that Rust's futures do not do anything unless polled. The whole system is built around this: for example, cancellation is dropping the future for precisely this reason. In contrast, in other languages, calling an async fn spins up a future that starts executing immediately.
A point about this is that async & await in Rust are not inherently concurrent constructions. If you have a program that only uses async & await and no concurrency primitives, the code in your program will execute in a defined, statically known, linear order. Obviously, most programs will use some kind of concurrency to schedule multiple, concurrent tasks on the event loop, but they don't have to. What this means is that you can - trivially - locally guarantee the ordering of certain events, even if there is nonblocking IO performed in between them that you want to be asynchronous with some larger set of nonlocal events (e.g. you can strictly control ordering of events inside of a request handler, while being concurrent with many other request handlers, even on two sides of an await point).
This property gives Rust's async/await syntax the kind of local reasoning & low-level control that makes Rust what it is. Running up to the first await point would not inherently violate that - you'd still know when the code executed, it would just execute in two different places depending on whether it came before or after an await. However, I think the decision made by other languages to start executing immediately largely stems from their systems which immediately schedule a task concurrently when you call an async fn (for example, that's the impression of the underlying problem I got from the Dart 2.0 document).
Dart 2.0 的一些背景由 this discussion from 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.
cramert replies(注意有些语法现在已经过时了):
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; // 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>
的东西是 returned。
就是这样。 "actual" 工作尚未完成。如果您获取 foo
的结果并将其推向完成(通过轮询,在本例中是通过 futures::executor::block_on
),那么接下来的步骤是:
实现
Future<Output = u8>
的东西是 return 通过调用long_running_operation
编辑的(它还没有开始工作)。another_operation
确实有效,因为它是同步的。.await
语法导致long_running_operation
中的代码开始。foo
未来将继续 return "not ready" 直到计算完成。
输出将是:
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
块中,然后等待两个操作完成,然后此功能才会完成。
请注意,像这样包装同步代码不是对于实际需要很长时间的任何事情的好主意;有关详细信息,请参阅
有线程池
// 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
}
考虑这个简单的伪JavaScript代码获取一些数据,处理它,根据上一步获取更多数据,总结它,然后打印结果:
getData(url)
.then(response -> parseObjects(response.data))
.then(data -> findAll(data, 'foo'))
.then(foos -> getWikipediaPagesFor(foos))
.then(sumPages)
.then(sum -> console.log("sum is: ", sum));
以async/await
形式,即:
async {
let response = await getData(url);
let objects = parseObjects(response.data);
let foos = findAll(objects, 'foo');
let pages = await getWikipediaPagesFor(foos);
let sum = sumPages(pages);
console.log("sum is: ", sum);
}
它引入了很多一次性变量,可以说比带有 promise 的原始版本差。那么何必呢?
考虑这一变化,稍后在计算中需要变量 response
和 objects
:
async {
let response = await getData(url);
let objects = parseObjects(response.data);
let foos = findAll(objects, 'foo');
let pages = await getWikipediaPagesFor(foos);
let sum = sumPages(pages, objects.length);
console.log("sum is: ", sum, " and status was: ", response.status);
}
并尝试用 promises 以原始形式重写它:
getData(url)
.then(response -> Promise.resolve(parseObjects(response.data))
.then(objects -> Promise.resolve(findAll(objects, 'foo'))
.then(foos -> getWikipediaPagesFor(foos))
.then(pages -> sumPages(pages, objects.length)))
.then(sum -> console.log("sum is: ", sum, " and status was: ", response.status)));
每次需要回溯之前的结果时,都需要将整个结构嵌套得更深一层。这很快就会变得很难阅读和维护,但是 async
/await
版本没有这个问题。
Rust 中 async
/await
的目的是提供并发工具包——与 C# 和其他语言相同。
在 C# 和 JavaScript 中,async
方法立即开始 运行,并且无论您 await
结果与否,它们都会被安排。在 Python 和 Rust 中,当您调用 async
方法时,在您 await
之前什么都不会发生(它甚至没有被安排)。但无论哪种方式,它在很大程度上都是相同的编程风格。
生成另一个任务(与当前任务并发且独立于当前任务运行)的能力由库提供:参见 async_std::task::spawn
and tokio::task::spawn
。
至于为什么 Rust async
不完全像C#,好吧,考虑一下两种语言的区别:
Rust 不鼓励全局可变状态。 在 C# 和 JS 中,每个
async
方法调用都隐式添加到全局可变队列中。这是对某些隐式上下文的副作用。不管是好是坏,这不是 Rust 的风格。Rust 不是框架。 C# 提供默认事件循环是有道理的。它还提供了一个很棒的垃圾收集器!许多其他语言中的标准内容是 Rust 中的可选库。