top | item 20719095

Async-std: an async port of the Rust standard library

349 points| JoshTriplett | 6 years ago |async.rs

234 comments

order
[+] 2bitencryption|6 years ago|reply
I must be dumb, because every time I dive into async/await, I feel like I reach an epiphany about how it works, and how to use it. Then a week later I read about it again and totally lost all understanding.

What do I gain if I have code like this [0], which has a bunch of `.await?` in sequence?

I know .await != join_thread(), but doesn't execution of the current scope of code halt while it waits for the future we are `.await`-ing to complete?

I know this allows the executor to go poll other futures. But if we haven't explicitly spawned more futures concurrently, via something like task::spawn() or thread::spawn(), then there's nothing else the cpu can possible do in our process?

[0] https://github.com/async-rs/async-std/blob/master/examples/t...

[+] zamadatix|6 years ago|reply
A good example is say you want to handle 100k TCP sessions concurrently. You probably don't want to launch 100k threads considering the overhead in doing so and constantly switching between them. You also don't want to do things synchronously as you'll constantly be waiting on pauses instead of doing work on the 100k sessions. So you launch 100k instances of it as an async function and they all stay in a single thread (or couple of threads if you want to utilize multiple cores for the work) and instead of constantly waiting on pauses it simply works on the log of queued up events.

Same code flow just it allows you to launch the same thing multiple times without having to wait for the whole thing to finish sequentially or wait on the OS to handle your threads.

[+] cheez|6 years ago|reply
async/await are coroutines and continuations (bear with me).

Here is synchronous code:

    result = server.getStuff()
    print(result)
Here is synchronous code, that tries to be asynchronous:

    server.getStuff(lambda result: print(result))
Once server.getStuff returns, the callback passed to it is called with the result.

Here is the same code with async/await:

    result = await server.getStuff()
    print(result)
Internally, the compiler rewrites it to (roughly) the second form. That's called a continuation.

That's pretty much it.

A more involved example.

Synchronous code:

    result = server.getStuff()
    second = server.getMoreStuff(result+1)
    print(result)
Synchronous code that tries to be asynchronous:

    server.getStuff(
        lambda result: server.getMoreStuff(
          result+1, 
          lambda result2: print(result2)
    ))
A lot of JS code used to look like this hideous monstrosity.

Async/await version:

    result = await server.getStuff()
    second = await server.getMoreStuff(result+1)
    print(result)
Remember again, that it is basically transformed by the compiler into the second form.
[+] ijidak|6 years ago|reply
So, the power of async await is no greater than the thread pool manager sitting under it.

You are correct. In edge cases where there is only 1 await in the queue for 1 process with 1 thread you gain nothing.

But you're accurately describing an edge case where await has limited value.

Await's true power shows up when you anticipate having multiple in-flight operations that all will, at overlapping points, be waiting on something.

Rather than consume the current thread while waiting, you're telling the run-time, go ahead and resume another task that has reached the end of its await.

This was possible before using various asynchronous design patterns, but all of them were clunky, in that they required boilerplate code to do what the compiler should be able to figure out on its own:

"Hey, runtime. This is an asynchronous call. Go do something useful with this thread and get back to me."

Second, await is MUCH EASIER for future developers to process because it looks exactly like any other method call and makes it easy to reason about the logic flow of the code.

Rather than chasing down async callbacks and other boilerplate concepts to manually handle asynchronous requests, the code reads like its synchronous twin.

int a = await EasyToFollowAsyncIntent();

This makes the code much easier to reason about.

To me those are the 2 biggest gains from async.

1. Less boilerplate code for asynchronous calls.

2. Code remains linearly readable despite being highly asynchronous.

[+] Argorak|6 years ago|reply
In small examples like this, you don't gain anything. For the sake of the example, we just run one task. But you _could_ run 100 with them. And at each of those `awaits`, they could schedule differently.

For a more complex networked application, we have the tutorial here: https://github.com/async-rs/a-chat

[+] weavie|6 years ago|reply
Yeah if there are no other futures spawned, then the await is going to cause the app to just sit there until the future completes. It's got nothing better to do.

If there was another future spawned then the await would cause the runtime to sit there until either of the futures completed. The code would attend to the first future that completes intil that hits an await.

[+] esotericn|6 years ago|reply
Right, that specific instance is essentially a single-threaded* application.

Now imagine that you spawned a few hundred of them with JoinAll. Each would run, multiplexed within a single thread, with execution being passed at the await points.

* anyone know the correct nomenclature for this? Single-coroutine?

[+] MusharibSajid|6 years ago|reply
Async is a hard concept but what can be revealing is going through the three steps:

1. Get used to callbacks in NodeJS, for example write some code using fs that reads the content of a file, then provide a callback to print that content.

2. Get used to promises in NodeJS, for example turn the code in #1 into a promise by creating a function with that calls the success/reject handler as appropriate on the callback from opening that file. Then use the promise to open the file and use .then(...) to handle the action.

3. Now do it in async. You have the promise, so you just need to await it and you can inline it.

By doing it in the 3 steps I find it is more clear what is really happening with async/await.

[+] nurettin|6 years ago|reply
Say you want to listen to a socket and also receive input from the keyboard at the same time.

If you have an async method that can wait for input on both devices, you can await the results of both of them, and they won't block eachother.

[+] MaxBarraclough|6 years ago|reply
> I must be dumb

Nope, async really isn't trivial.

> I know .await != join_thread(), but doesn't execution of the current scope of code halt while it waits for the future we are `.await`-ing to complete?

It doesn't, that's the charm of it.

It's best to treat 'await' as syntactic sugar, and to dig in to the underlying concepts.

I realise we're not talking C#/.Net, but that's what I know: in .Net, your function might do slow IO (network activity, say) then process the result to produce an int. Your function will have a return-type of `Task<int>`. Your function will quickly return a non-completed Task object, which will enter a completed state only once the network activity has concluded and processing has occurred to give the final `int` value.

The caller of your function can use the `Task#ContinueWith` method, which enqueues work to occur if/when the Task completes, using the result value from the Task. (We'll ignore exceptions here.)

Internal to your function, the network activity itself will also have taken the form of a standard-library Task, and our function will have made use of its `ContinueWith` method. Things can compose nicely in this way; `Task#ContinueWith` returns another Task.

(We needn't think about the particulars of threads too much here, but some thread clearly eventually marks that Task object as completed, so clearly some thread will be in a good position to 'notice' that it's time to act on that `ContinueWith` now. The continuation generally isn't guaranteed to run on the same thread as where we started. That's generally fine, with some notable exceptions.)

You might think that chain-invoking `ContinueWith` would get tedious, as you'd have to write a new function for each step of the way if we make use of several async operations - each continuation means writing another function to pass to `ContinueWith`, after all. Perhaps it would be more natural to just write one big function and have compiler handle the `ContinueWith` calls.

You'd be right. That's why they invented the `await` keyword, which is essentially just syntactic sugar around .Net's `ContinueWith` method. It also correctly handles exceptions, which would otherwise be error-prone, so it's generally best to avoid writing continuations manually.

There's more machinery at play here of course, but that seems like a good starting point.

Assorted related topics:

* If you use `ContinueWith` on a Task which is already completed, it can just stay on the same thread 'here and now' to run your code

* It's possible to produce already-completed Task objects. Rarely useful, but permitted.

* There's plenty going on with thread-pools and .Net 'contexts'

* The often-overlooked possibility of deadlocking if you aren't careful [0]

* None of this would make sense if we had to keep lots of background threads around to fire our continuations, but we don't [1]

* Going async is not the same thing as parallelising, but Tasks are great for managing parallelism too

* This stuff doesn't improve 'straight-line' performance, but it can greatly improve our scalability by avoiding blocking threads to wait on IO. (That is to say, we can better handle a high rate of requests, but our speed at handling a lone request on a quiet day, will be no better.)

I found this overview to be fairly digestible [2]

[0] https://blog.stephencleary.com/2012/07/dont-block-on-async-c...

[1] https://blog.stephencleary.com/2013/11/there-is-no-thread.ht...

[2] https://stackoverflow.com/a/39796872/

See also:

https://docs.microsoft.com/en-us/dotnet/standard/parallel-pr...

https://docs.microsoft.com/en-us/dotnet/api/system.threading...

[+] cryptonector|6 years ago|reply
async/await is all about letting you write serial looking code with the smallest memory footprint short of writing hand-coded continuation passing style (CPS) code.
[+] justicezyx|6 years ago|reply
User space threads and corouting is pretty much the right abstraction for application code.

Async can be useful when more control over the details of execution is needed.

[+] Un1corn|6 years ago|reply
This remind me of the blog post "What Color is Your Function?"[0], they had to create a different library that is the same as the standard library but with async functions.

I thought Rust had other, better ways to create non-blocking code so I don't understand why to use async instead.

[0] https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...

[+] pcwalton|6 years ago|reply
> I thought Rust had other, better ways to create non-blocking code so I don't understand why to use async instead.

In fact, Rust does have a great solution for nonblocking code: just use threads! Threads work great, they are very fast on Linux, and solutions such as goroutines are just implementations of threads in userland anyway. (The "what color is your function?" post fails to acknowledge that goroutines are just threads, which is one of my major issues with it.) People tell me that Rust services scale up to thousands of requests per second on Linux by just using 1:1 threads.

Async is there for those who want better performance than what threads/goroutines/etc. can provide. If you don't want to deal with two "colors" of functions, you don't have to! Just use threads.

[+] bryanlarsen|6 years ago|reply
None of the 5 points in that article about callbacks in 2015 node.js apply to async in Rust. The Rust people spent years agonizing over their version of async and applied a lot of lessons learned from implementations in other languages.

https://news.ycombinator.com/item?id=20676641

It's trivial to turn async into sync in Rust. You can use ".poll", "executor::block_on", et cetera.

Turning sync into async is harder in any language. Even Go with it's easy threading. That's a good argument to make async the default in libraries in Rust, but since async isn't stable yet, that would have been hard to do 5 years ago.

[+] dom96|6 years ago|reply
There are ways to get around this, the way that I have done it in Nim is via a `multisync` macro:

    proc readLine(s: Socket | AsyncSocket): Future[string] {.multisync.} =
      while true:
        let c = await s.recv(1)
        case c
        of '\n':
          return
        else:
          result.add(c)
This is equivalent to defining two `readLine` procedures, one performing synchronous IO and accepting a `Socket` and another performing asynchronous IO and accepting an `AsyncSocket`. It works very well in practice.
[+] zzzcpan|6 years ago|reply
The point of asynchronous programming is to know exactly where concurrency happens in your code. This both eliminates concurrency bugs and gives you predictability for high performance.
[+] pohl|6 years ago|reply
The caller of the function knows nothing about what happens within the body of the function. (Is it just doing computation, or is it doing I/O?). The async keyword is how the author of the function makes it explicit that caller should choose when to await the result.

Isn't the alternative WCiYF is proposing to allow the caller to treat any function asynchronously, while having no way to discern whether doing so might be counterproductive?

[+] lmm|6 years ago|reply
The right way to handle this stuff is polymorphism. But with Rust lacking higher-kinded types I guess that's not possible. A decent test of whether their "associated type constructors" actually solve the same problems, as sometimes claimed, would be whether you can write this kind of async-polymorphic code with them.
[+] otabdeveloper2|6 years ago|reply
> I thought Rust had other, better ways to create non-blocking code so I don't understand why to use async instead.

'async' exists because Python has that GIL bullshit and so Python programmers had to invent that fifth wheel of 'async programming'.

Programmers in other languages then got jealous because they, too, wanted a complex, unnecessary framework that pollutes the whole runtime and serves to differentiate regular programmers from 'rockstar' programmers.

And so async got fashionable and barely-literate coders now think async is magic performance dust that will automatically make your program run 1000% faster.

TL;DR - it's just fashion, give it five years and we'll be reading posts about how async sucks and that it's stupid legacy tech invented by bonehead dinosaurs.

[+] limsup|6 years ago|reply
It looks great.

But it's odd that they do not cite Tokio. I know this isn't an academic paper, but come on have some professional curtesy and discuss the contributions made in prior art.

[+] ShinTakuya|6 years ago|reply
Apologies if I'm misunderstanding things here, I'm just now getting back into Rust after a couple of years of not using it. Did Tokio really inspire this library that much?
[+] evmar|6 years ago|reply
In case anyone else was curious how you create nonblocking file I/O, it appears to use threads.

I am curious if the number of threads is unbounded, or if they have a bounded set but accept deadlocks, or if there is a third option other than those two that I am unaware of.

[+] slovenlyrobot|6 years ago|reply
io_uring grew support for buffered IO in recent kernels, so we should have widespread support for this in userspace circa 2025
[+] rmgraham|6 years ago|reply
Are those really the only options? I'm trying to wrap my head around how using a fixed size thread pool for I/O automatically implies deadlocks but I just can't. Unless the threads block on completion until their results are consumed instead of just notifying and then taking the next task..

I can definitely imagine blocking happening while waiting for a worker to be available, though. Did you mean simply blocking instead of deadlock?

[+] layoutIfNeeded|6 years ago|reply
Non-blocking I/O via threads? That’s what we used to call blocking I/O :D
[+] jedisct1|6 years ago|reply
The documentation is great, and the API documentation includes examples for many functions. This is really appreciable. Thank you for that!
[+] rammy1234|6 years ago|reply
Anything Rust gets the post to number 1 spot. What makes Rust special that other programming languages don't enjoy ?
[+] hathawsh|6 years ago|reply
Rust is very ambitious and unusually successful at reaching its ambitions. It's efficient like C/C++, but safer. It's modern like Go, but more expressive and open to metaprogramming. It's often as readable as a scripting language, but doesn't depend on garbage collection. It's a young rising star originating from a great company.
[+] topspin|6 years ago|reply
> Anything Rust gets the post to number 1 spot

This is bunk. Simply run this search[1] and behold the stream of Rust related submissions that get no play at all; zero comments and no more than one or two up votes. These instances of highly ranked rust stories are actually the exception; no more than one or two a week typically. The rest of the Rust stuff is seen by almost no one.

[1] https://hn.algolia.com/?query=rust&sort=byDate&prefix&page=1...

[+] jnbiche|6 years ago|reply
Rust is the first language basically ever that has a serious chance at displacing C and C++. Combined with a type system that eliminates multiple error classes, and some functional aspects, it's a very interesting language.
[+] pradn|6 years ago|reply
1) It's built in large part by Mozilla, which enjoys special love as an OSS company. 2) Rust provides tangible benefits (memory/concurrency safety) over existing languages in an important niche (performance-sensitive programs). 3) The community is nice and talented so it's fun to see what they're up to.
[+] monocasa|6 years ago|reply
Deterministic memory and object deallocation combined with memory safety, and a vibrant open ecosystem.
[+] brians|6 years ago|reply
A possibility to displace C and Java.

Features like Haskell—destructuring bind, useful type system.

Performance like C, including no GC.

[+] ggregoire|6 years ago|reply
Strong static typing with types inference. Memory allocation manually handled, no garbage collector. C-like performances.

Makes it a good option when reliability and performance matter (think web browser, database or anything at the OS level).

[+] jedisct1|6 years ago|reply
How does it balance tasks across CPU cores?

The thing I like in Go is that I don’t have to worry about that, it’s all automatic.

[+] Argorak|6 years ago|reply
It does the scheduling for you. That's why all Futures put onto task through `async_std::task` must be `Send`. That's Rust parlance for "can be safely migrated between threads".

It's not Go, but we know what people like about Go. <3

[+] pythonist|6 years ago|reply
I am wondering that also, scheduler is of most importance.

Go is such a joy to work with.

[+] littlestymaar|6 years ago|reply
Has preemptive scheduling landed in Go ? Because last time I worked with it (Go 1.10) it was still cooperative and you had to worry about it otherwise you could get bitten badly.
[+] pcwalton|6 years ago|reply
In Rust you use async/await with a scheduler like mio that will automatically do that for you as well.
[+] davidhyde|6 years ago|reply
Great library, well done. In case anyone was wondering this is not a [no_std] crate even though it can be used as a replacement for std library calls. I guess it (obviously) can't be since it interfaces with the operating system so much.
[+] Argorak|6 years ago|reply
Project member here.

It exports stdlib types (like io::Error) where appropriate so that libraries working with these can stay compatible, so `no_std` is not really an option.

The underlying library (async-task) is essentially core + liballoc, just no one made the effort to spell that out, yet.

[+] bpye|6 years ago|reply
I really wish we could work out a better way to make no_std easier. I know why io::Error requires std for example, but it makes things difficult.

It would be nice if you could provide your own sys crate so you could even use some of std on an embedded device. If you had say an RTC you could make time related calls work, maybe you'd wire networking to smoltcp etc. Currently you could do that - maybe - but you'd have to modify the Rust standard library.

[+] bfrog|6 years ago|reply
very cool! Now we need a dpdk equivalent to really blow away expectations people have of what is fast