Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Async/await pattern always confuses me, someone please let me know if I get this right:

First, async/await does NOT mean "threading" or "multiprocessing" or "concurrency". It simply means "using a state machine to alternate between tasks, which may or may not be concurrent." Right?

Further, in Javascript, futures and async are utilized heavily because we so frequently need to wait for IO events (i.e.: network events) to complete, and we don't want to block execution of the entire page just to wait for a IO to complete. So the JS engine allows you to fire off these network events, do something else in the meantime, and then execute the "done" behavior when the IO is complete (and even in this case, we might not be concurrent, because ).

That makes sense to me.

But say I have written something in Rust that makes use of async/await. And say there is absolutely no IO or multithreading. Say I have some awaitable function called "compute_pi_digits()" that can take arbitrarily long to complete but does not do IO, it's purely computational. Is there any benefit to making this function awaitable? Unless I actually spawn it in a different thread, the awaitable version of this function will behave identically to if it were NOT awaitable, correct?

And one last idea: the async/await pattern is becoming so popular across vastly different languages because it allows us to abstract over concepts like concurrency, futures, promises, etc. It's a bit of a "one size fits all" regardless of whether you're spinning up a thread, polling for a network event, setting up a callback for a future, etc?



I think the JS world might be particularly excited about async/await because other people just escape to threads when they don't want to block the whole thing on IO.

Both in JS or Rust, you don't gain anything just by declaring your thing to be async, or awaitable. Your function needs to be built around some kind of "primitive" that explicitly supports the "do something else in the meantime" mechanism. Using "await" on that thing lets your function piggyback on its support, but all your explicit, normal code is synchronously blocking as usual.

In Rust, I think it's a fairly established pattern to turn blocking code, where the blocking part is not some IO action that has explicit support for the futures mechanism, into a asynchronous, awaitable function by punting the work to a threadpool. That makes sense for CPU-bound work as well as IO done by libraries that don't support futures or things like disk IO where the OS might not actually have decent support for doing it in a non-blocking fashion.

I'm not sure if it's the canonical mechanism, but this crate seems to implement what I'm thinking of: https://docs.rs/futures-cpupool/0.1.8/futures_cpupool/


The latest tokio has a work-stealing threadpools, so you don’t need to explicitly do this anymore, IIRC.


Neat! Do you know if there's anything in there resembling the rayon::join API? I didn't see anything on a cursory look.


I am not sure, to be honest. I've been waiting until everything settles down to really dig in.


> Both in JS or Rust, you don't gain anything just by declaring your thing to be async, or awaitable.

I'm not familiar with Rust but in JS you do gain something. Just the fact that the function is async means that it now explicitly returns a promise, which means that anything awaiting that promise will be in a new execution context and will definitely not run synchronously.


> Is there any benefit to making this [compute_pi_digits()] function awaitable?

If you introduce suspension points in that (e.g. every 100 computed digits), then you can co-schedule other tasks (e.g. a similar `compute_phi_digits`) or handle graceful cancellation (e.g. if a deadline is exceeded, or its parent task aborted in the meanwhile).


> And say there is absolutely no IO

Well, then we can simply optimize away your entire program as it does nothing and running it has no side effects.

Even if your program is entirely CPU bound, there are uses for writing in an async/await style. As an example, parsers can be quite natural to write in that style.

Or you can use it to have multiple computations running at the same time, and give updates on their progress. It is voluntary time slicing, which is significantly less overhead than the OS doing time slicing for you.


CPUs do the physical calculation and the number of cores determines the number of calculations that can happen at the same time.

Software threads, from the OS to your application, run on one of these cores for a certain amount of scheduled time, then get switched out with some other thread. If threads are waiting on IO then they're not making much use of the time they get and are wasting CPU capacity.

An older approach is to just make more threads and switch them out faster, but this is very inefficient. Await/async is a way to let threads not get stalled by a single function and switch to a different function in that process that does have work available. It's basically another step of granularity in slicing CPU time within a thread.

The keywords do not force anything, they are just signals to the underlying software that it may pause and come back later if necessary, along with setting up state to track results. Some methods may still run all on the same thread if there's nothing else to do, or if the async result is already available and there is no waiting needed.

Most async/await is usually built on top of yield, generators, promises or other constructs that basically are state-machines or iterators.


Incremental computation can be useful for better responsiveness even if you only have one thread. A simple example in JavaScript would be a Mandelbrot viewer, where you don't want to lock up the UI doing a heavy computation. So, you could have something that looks like an async call that really does the heavy computation in small chunks using idle callbacks, and the future completes when it'd done. (Using a background worker thread is probably better though.)

The async call itself doesn't return intermediate results, though, so you'd have to handle that a different way. And if you want to cancel the task, you need another way to handle that too.

Something like computing the digits of pi would be better represented by a stream or iterator since the caller should decide when it's done.


That's called cooperative multitasking, and it was roughly how things worked in the Windows 3.1 era. Nowadays it's much better to use real threads, as they much better protect against latency (responsiveness) issues.


Concrete example right on the Dart homepage: https://www.dartlang.org/


The correct way to deal with that problem is to decouple the UI from the computation, once process for each would be the ideal, anything less is going to messy and require all kinds of hacks to give the same outward appearance. Better still: one supervisor process and one for UI and computation each.


Well, in a web app you don't get to make those choices, but you could use a web worker.


It's not about IO, it's about any long-running operation that can happen in a background thread. JS developers just think it is about IO because that's the only thing (web workers notwithstanding) in JS-land that can run in another thread.


It would be misleading for consumers of the interface to mark compute_pi_digits awaitable -- at least in .NET world that have quite a lot of experience with async-await at this point.

See http://blog.ploeh.dk/2016/04/11/async-as-surrogate-io/ for further discussion. Regular programmers are not using Task<T> as IO-monadic marker consciously, but they are surprised when a usage differs from that model.


Look into continuation-passing style. Semantically, async/await is much like syntactic sugar for CPS (or rather futures, but at the most basic level they can be thought of as single-shot continuations).

But ultimately, to make use of async, you need async primitives - something that lets you say "do this in the background somehow, and let me know once you're done". Any async/await call should ultimately end at one of those primitives, and it's at that point that another call might get interleaved. If you don't actually do I/O or anything else that can do a non-blocking wait, you're not getting anything useful from async.


It's only semantically CPS in as much as all code is semantically CPS. Thinking about the parallels with CPS does nothing to help, here. Async/await is just like threaded code with the blocking/concurrent calls specially marked.


Except it's not like threaded code, because there aren't necessarily multiple threads. And with a single thread event loop, you don't need locking and other synchronization mechanisms at all, further reinforcing the point.

Async/await is literally all about explicit continuations. It's not about concurrency or parallelism per se, although it can be used in that context.


That's how I understand it.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: