In a language like C#, giving this code (I am not using the await
keyword on purpose):
async Task Foo()
{
var task = LongRunningOperationAsync();
// Some other non-related operation
AnotherOperation();
result = task.Result;
}
In the first line, the long operation is run in another thread, and a Task
is returned (that is a future). You can then do another operation that will run in parallel of the first one, and at the end, you can wait for the operation to be finished. I think that it is also the behavior of async
/await
in Python, JavaScript, etc.
On the other hand, in Rust, I read in the RFC that:
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.
In this situation, what is the purpose of async
/await
in Rust? Seeing other languages, this notation is a convenient way to run parallel operations, but I cannot see how it works in Rust if the calling of an async
function does not run anything.
The purpose of
async
/await
in Rust is to provide a toolkit for concurrency—same as in C# and other languages.In C# and JavaScript,
async
methods start running immediately, and they're scheduled whether youawait
the result or not. In Python and Rust, when you call anasync
method, nothing happens (it isn't even scheduled) until youawait
it. But it's largely the same programming style either way.I think you're right that the ability to spawn another task (that runs concurrent with and independent of the current task) is a missing piece. Maybe it'll be added. (Remember, Rust's
async
is not done yet—the design is still evolving.)As for why Rust
async
is not exactly like C#, well, consider the differences between the two languages:Rust discourages global mutable state. In C# and JS, every
async
method call is implicitly added to a global mutable queue. It's a side effect to some implicit context. For better or worse, that's not Rust's style.Rust is not a framework. It makes sense that C# provides a default event loop. It also provides a great garbage collector! Lots of things that come standard in other languages are optional libraries in Rust.
Consider this simple pseudo-JavaScript code that fetches some data, processes it, fetches some more data based on the previous step, summarises it, and then prints a result:
In
async/await
form, that's:It introduces a lot of single-use variables and is arguably worse than the original version with promises. So why bother?
Consider this change, where the variables
response
andobjects
are needed later on in the computation:And try to rewrite it in the original form with promises:
Each time you need to refer back to a previous result, you need to nest the entire structure one level deeper. This can quickly become very difficult to read and maintain, but the
async
/await
version does not suffer from this problem.You are conflating a few concepts.
Concurrency is not parallelism, and
async
andawait
are tools for concurrency, which may sometimes mean they are also tools for parallelism.Additionally, whether a future is immediately polled or not is orthogonal to the syntax chosen.
async
/await
The keywords
async
andawait
exist to make creating and interacting with asynchronous code easier to read and look more like "normal" synchronous code. This is true in all of the languages that have such keywords, as far as I am aware.Simpler code
This is code that creates a future that adds two numbers when polled
before
after
Note that the "before" code is basically the implementation of today's
poll_fn
functionSee also Peter Hall's answer about how keeping track of many variables can be made nicer.
References
One of the potentially surprising things about
async
/await
is that it enables a specific pattern that wasn't possible before: using references in futures. Here's some code that fills up a buffer with a value in an asynchronous manner:before
This fails to compile:
after
This works!
Calling an
async
function does not run anythingThe implementation and design of a
Future
and the entire system around futures, on the other hand, is unrelated to the keywordsasync
andawait
. Indeed, Rust has a thriving asynchronous ecosystem (such as with Tokio) before theasync
/await
keywords ever existed. The same was true for JavaScript.Why aren't
Future
s polled immediately on creation?For the most authoritative answer, check out this comment from withoutboats on the RFC pull request:
Some of the Dart 2.0 background is covered by this discussion from munificent:
cramert replies (note that some of this syntax is outdated now):
Code examples
These examples use the async support in Rust 1.39 and the futures crate 0.3.1.
Literal transcription of the C# code
If you called
foo
, the sequence of events in Rust would be:Future<Output = u8>
is returned.That's it. No "actual" work is done yet. If you take the result of
foo
and drive it towards completion (by polling it, in this case viafutures::executor::block_on
), then the next steps are:Something implementing
Future<Output = u8>
is returned from callinglong_running_operation
(it does not start work yet).another_operation
does work as it is synchronous.the
.await
syntax causes the code inlong_running_operation
to start. Thefoo
future will continue to return "not ready" until the computation is done.The output would be:
Note that there are no thread pools here: this is all done on a single thread.
async
blocksYou can also use
async
blocks:Here we wrap synchronous code in an
async
block and then wait for both actions to complete before this function will be complete.Note that wrapping synchronous code like this is not a good idea for anything that will actually take a long time; see What is the best approach to encapsulate blocking I/O in future-rs? for more info.
With a threadpool