Hacker News new | past | comments | ask | show | jobs | submit login
Await in Rust (docs.rs)
368 points by luu on Aug 12, 2019 | hide | past | favorite | 243 comments



Reading through comments on async/await-related articles, I wonder if I'm the only person who find the whole concept of async/await utterly weird. Specifically I have troubles embracing the need to mark function as "async" – it just doesn't make sense to me. Synchronous or asynchronous is something up to the caller to the decide – not to the function itself.

Like in real life I can do something synchronously (meaning paying full attention), or I can do the same task asynchronously (watching youtube videos in the background while cooking, for example), but it's still up to me. Being "async" is not a property of the "watching youtube" action, it's the property of the caller running this action.

That's the reason why CSP-based concurrency models really works well for me – it's just so easy to map mental models of system's behaviour from the head directly to the code. You have function/process and it's up to you how do you run it and/or how synchronize with it later.

Async/await concept so popular in modern languages is totally nuts in this aspect. Maybe it's just me, but I find myself adding more accidental complexity to the code just to make async/await functions work nicely, especially for simple cases where I don't need concurrency at all, but one "async" function creeps in.


> Synchronous or asynchronous is something up to the caller to the decide – not to the function itself.

async/await aren't strictly necessary, but to avoid them you need one of:

1. a sufficiently smart compiler with whole program compilation, or

2. to compile two versions of every function, an async variant and a sync variant, or

3. every async wait captures the whole stack, thus wasting a lot of memory.

Async/await is basically a new sort of calling convention, where the program doesn't run in direct style but in continuation-passing style. This permits massive concurrency scaling with little memory overhead, but there other tradeoffs as per above.

Async/await makes perfect sense for Rust which wants to provide zero-overhead abstractions.


None of the things you mention is necessary. In fact, we're adding user-mode threads (based on delimited continuations) to the JVM without doing any of them. Remember that every subroutine must already be compiled into a state machine or you wouldn't be able to return to it. I.e., every subroutine is already compiled in just the right way to be suspended already. The reason some languages run into problems are:

1. They may not have access to the representation the subroutines store their state, as it may be platform dependent (e.g. if you're targeting LLVM you may not have access to that representation).

2. They may not track their pointers, and so find it hard to move chunks of stack around if they have internal pointers into the stack.

3. They may want to enable some specific optimizations in the special (and rare, especially when IO is involved) case of a monomorphic calls to shallow coroutines. This requires that the representation of the subroutine's state be available as a high-level node for the compiler to change.


The JVM doesn't ever lose access to the bytecode, but Rust compiles down to machine code. The garbage collector and heap-oriented memory allocation without borrowing means you don't have nearly as much "stack" to worry about, and copying references is always OK.

For a statically compiled language that produces machine code and prefers stack allocation and has strict constraints on copying references, it is not so easy.


Bytecode is irrelevant, but yeah, as I wrote, internal pointers into the stack do make moving stacks around difficult.


Reusing the stack frame layout for a continuation state block is a really neat approach!

Internal pointers into the stack aren't really the reason Rust can't take this approach either, though, unfortunately. Async frames are "pinned" once they start running anyway so the ecosystem is designed around not needing to move them. Async callee frames are, in a sense, contained in their caller's frames- this makes async recursion tricky, but in exchange each static call graph uses a single allocation.

The real issue is basically naasking's #1- not that Rust loses its IR or that it's platform dependent, but that LLVM simply doesn't have the infrastructure to work with stack frames directly. A sufficiently motivated+funded team could add it but Rust doesn't have that luxury.

In fact, this is also the reason that C++20 coroutines individually heap-allocate their frames. No compiler had the infrastructure or was able to put in the work in time. There are also some thorny problems around phase ordering here- stack frame layout typically isn't determined until very very late (to enable optimizations) but program-accessible types in both C++ and Rust need to know their sizes very very early. Java can sidestep this by being slightly higher level.


Java sidesteps this because we generate concrete machine instructions and have control over the low-level platform-specific representation. In other words, because our backend is lower level, not higher level. But yeah, if you don't have access to the low level representation -- like Rust and Kotlin -- you don't really have a choice except to wait for your backend to give you that access. Sometimes waiting is a good approach, though. I'm not even sure you need control over the stack representation; it's sufficient to know what it is, which you could maybe do even on top of LLVM (but I don't know LLVM well enough to say for sure).


Yeah, exactly. I believe you're correct that you just need to know what the stack looks like, not really change it much if at all. Any tweaks you may need to make are probably already available for things like split stacks or green threads.

The problem is that LLVM doesn't even let you see the stack layout until very late, and then only as debug info. You could probably hack something on top but it would probably wind up looking a lot like C++20, with a lot of extra allocation, or like Rust, with all the stack frame layout optimization duplicated into the frontend.


It isn't irrelevant for point #2 above yours, which is that the JVM could rewrite and run/JIT two versions of every function. The JVM monomorphizes plenty already, does it not?


It could compile two versions of every function on demand, but there's no need to (although it could be an implementation choice). AFAIK, every compiler in the world already compiles functions into state machines that are suitable for delimited continuations. What would the other version be for?


I'm not convinced they are suitable as-is in all compilers. Continuation capture can only be done at safe points similar to GC. The compiler and runtime need tight integration, which isn't a property of every compiler in the world. LLVM took a long time to integrate intrinsics to handle GC, for example.


"Physical" (i.e. non-inlined) subroutine calls are normally such safepoints (although I probably exaggerated when I said "every compiler in the world"). They need to be because a subroutine is a continuation. Upon return, it must be able to restore its state, otherwise it wouldn't be a subroutine. If you're doing any IO, there must such be such a physical call (and a safepoint) precisely where you need it -- at or before the actual IO.


It depends what you mean by "continuation capture".

If you just want to suspend some code, subroutine calls will do – but so will any other instruction, as long as you save the values of all registers. That's what the kernel does, after all. Using the subroutine call boundary lets you save a bit of work by saving only the callee-save registers, but it doesn't make that much difference. The problem is that you still need a stack, which prevents coroutines from being lightweight enough.

If you want to move the stack, like Go does... then just like with a GC, you need to track which registers and which stack offsets contain pointers at a given point in the program. That does require some sort of safepoint, which could be a subroutine call. But that's all irrelevant from Rust's perspective, because Rust's low-level pointer model is fundamentally incompatible with moving the stack. In Go, pointers into the stack are themselves only stored on the stack and in registers – never on the heap – so the runtime can easily find them all and adjust them. If you could store pointers into the stack onto the heap, it might still be possible to adjust them, albeit at higher cost, if you had a GC-managed heap where the runtime knows where all the pointers are. But Rust doesn't even have that. Tracking all the pointers in the heap is impossible for multiple reasons:

- Rust programs don't have to use any particular runtime-provided heap; they can ask the OS for memory directly and store pointers there.

- Pointers aren't necessarily stored in an easy-to-maintain way: e.g. Rust supports tagged unions, where the interpretation of one word in memory as either a pointer or integer depends on the value of another word.

Oh, and Rust lets you convert pointers to integers (useful for hashing purposes), so moving pointers behind a program's back would have visible side effects in any case.

So moving the stack is out. That leaves two possibilities:

- Segmented stacks;

- Calculating the required stack space statically.

LLVM can generate code with segmented stack support [1], and Rust used to enable that option. It's not that bad, but it does require every function to begin by checking whether there's enough space left for its stack, and call a runtime function (__morestack) if not. That adds a measurable overhead to function calls. It also requires using a custom thread-local storage slot to track the current stack base, which means you can only call the function on threads where the runtime set up that slot, not any random C thread. That's a problem if you're writing a library meant to be used by C code, which evolved into a major use case for Rust. For these reasons, Rust nixed segmented stack support sometime before 1.0. Re-enabling it is one hypothetical reason why you might want two versions of each function.

As for calculating the required stack space statically, that wouldn't require an alternate code generation mode. But as you were discussing in another subthread, the design of most compiler pipelines makes it very difficult to expose that information to the frontend. Even if that weren't a problem, merely making it theoretically possible to statically calculate stack usage requires imposing severe limits on the program. You can't make dynamic calls where you don't know the callee, and you can't use recursion at all (because then you might need infinite space). And if only a subset of code is async-compatible... then you end up with the equivalent of colored functions anyway. So even if LLVM's pipeline could be wrangled into doing what's necessary, it would only be a small improvement over the higher-level async/await implementation that Rust currently has. (As an alternative, you might be able to create a sort of hybrid mode where only dynamic and recursive calls use segmented stacks, but that would still require an alternate code generation mode.)

The nail in the coffin is that Rust also targets WebAssembly, where you can't do any of the nice low-level things that you can on real machines. This has also been a blocker for guaranteed tail call elimination. I hate it.

[1] https://llvm.org/docs/SegmentedStacks.html


> The problem is that you still need a stack, which prevents coroutines from being lightweight enough.

Async/await also needs the same logical stack, it just captures it frame by frame. Continuations can do the same. But as you say, Rust has some particular constraints that make this hard.

> then just like with a GC, you need to track which registers and which stack offsets contain pointers at a given point in the program

Well, you don't need to track all the pointers, just pointers into the stack.

> ... The nail in the coffin is that Rust also targets WebAssembly ...

But yeah, I mentioned some reasons for why it could be hard for some languages -- internal stack pointers and lack of control over the backend are two of them.

But I find it problematic that backend problems force feature design today. If a language wants to be popular 20 years from now it needs to have the patience not to bend to its backends' limitations, particularly as it has few competitors in its niche. So if a language's designers are absolutely certain that async/await is the right design for them for the next few decades, then that's great, but if they decide they want to design a language for the next few decades based on the limitations of WebAssembly in 2019, then that's something I like less. Sometimes there's no choice and a language/feature has to ship, but this is a feature that the world can and has lived without.


We have that implemented in Rust too. It's a user-mode thread implementation called mioco. It's not used very much because the performance tends to be worse than 1:1 threads.

This is a lesson that we all learned during the NPTL/NGPT era [1] and have subsequently forgotten and will have to relearn.

[1]: https://akkadia.org/drepper/nptl-design.pdf


So it's better to bet on something whose lesson is yet to be learned? :)

Anyway, I don't think anyone has forgotten those lessons, but they're not very relevant to modern managed runtimes. (also mioco seems to be single-threaded)


> So it's better to bet on something whose lesson is yet to be learned? :)

Async I/O isn't anything new, and it's stood the test of time. Nginx isn't some new experimental technology.

> Anyway, I don't think anyone has forgotten those lessons, but they're not very relevant to modern managed runtimes. (also mioco seems to be single-threaded)

They're still very relevant. Linux can spawn hundreds of thousands of kernel threads.

Anyway, we used to have an multithreaded M:N runtime in Rust, but it was removed because the performance was worse than 1:1 in practice. M:N was moved out of the standard library into the mioco library. Now it's unmaintained, because nobody actually wants M:N threads in Rust—an interesting outcome to be sure!


> Async I/O isn't anything new, and it's stood the test of time.

What do you mean? User-mode threads also use async I/O. They do just what async/await does, only without the syntax. I mean that async/await hasn't stood the test of time just yet.

> Linux can spawn hundreds of thousands of kernel threads.

Yeah, not very active ones, though. And their stacks are never really uncommitted. And the scheduler is badly optimized for many of the very uses you want in a server. On the other hand, you can spawn many millions of active fibers, schedule them how you like, and play with their state and its representation in many interesting ways (like moving a running fiber from one machine to another).

> Now it's unmaintained, because nobody actually wants M:N threads in Rust—an interesting outcome to be sure!

Just to make sure, when people say "M:N threads" they can mean a lot of very different things. In Java, they'll behave like async/await, only without the syntactic constructs. Stacks and stack frames are moved, can shrink and grow, can have various representations etc., and the scheduling is entirely pluggable. You can schedule the continuations manually, or in a scheduler of your choice, shared, or not, with ordinary tasks.


A clarification: Rust async/await stacks do not move or change size. They are fixed to the size of a static call graph and are pinned in place. (Going beyond that can be done with explicit heap allocation, but it's extremely rare in practice.)

This is, from what I can tell, their main advantage in performance over other implementations. (In Rust, at least- the performance story may easily be different in garbage collected languages like Go or Java.)

Another caveat: Rust's claims to have implemented M:N in the past must be tempered a bit by two factors. First, Rust had to use segmented stacks, which Go moved away from and which Loom seems to be avoiding, precisely because they're so expensive. Second, Rust had to dispatch all IO APIs dynamically based on the currently selected runtime, which Go avoids by not having two runtimes, and which Loom can probably avoid via its scheduler implementation and/or JIT devirtualization.


Early Rust used M:N exclusively and didn't need dynamic dispatch for I/O.


...but a large part of the reason to ditch M:N was the overhead it added to 1:1-style IO in a language quickly moving toward the C++ space.

They didn't decide to move off of M:N when it was exclusively M:N.


I was in the meetings where the decisions were made. The decision to add 1:1 in the first place was because of performance problems with M:N.


> In Java, they'll behave like async/await, only without the syntactic constructs. Stacks and stack frames are moved, can shrink and grow, can have various representations etc., and the scheduling is entirely pluggable. You can schedule the continuations manually, or in a scheduler of your choice, shared, or not, with ordinary tasks.

When you write "Java", you mean a specific implementation of the JVM, like the JVM reference implementation in OpenJDK, or you imply that all JVM implementations must support this to be called Java?

Edit: Is your comment related to Project Loom, aiming to add fibers and continuations to Java?


Yes.


Delimited continuations are a more restricted form of my #3. The captured stack is smaller, but still much larger than what's strictly necessary, and in the pathological case, I still think it can be up to the full stack size.

Unless you have some approach I haven't heard of, in which case, please provide a link!


Delimited continuations (I'm talking about one-shot, or non-reentrant continuations, AKA "stackful" coroutines) don't require more memory than async/await, as they capture the exact same state. That's the approach we're taking in Java, but I don't think it's particularly novel.


One shot delimited continuations capture strictly more state than a simple async/await implementation. The latter only captures bindings that are referenced after resumption, thus naturally implementing a sort of liveness analysis that discards unnecessary state. You could of course do the same thing during regular compilation, but it's not strictly necessary, and the interaction with GC means this state could potentially live much longer.

That said, the extra object headers from heap-allocated activations will make up some of the difference.


Why would this be the case? Normal stack layout and register allocation already discards things that are not live across function calls. (With some potential wiggle room because that's not the primary goal, I suppose?)


I guess that the savings obtained by a more aggressive spilling optimizations are more appealing in case of long running async state. In any case real workload measurements should be the only judge.


This liveness analysis isn't strictly necessary for async/await, either. You can do it in both cases or in none. In any event, it is not a matter of storing more stack, but of the level of optimization in the stack representation. Interaction with a GC brings its own complications as well as opportunities.


You're right that it's not strictly necessary, but as I said, it falls out naturally from computing the required continuations. This optimization doesn't necessarily happen naturally when compiling direct code, depending on your intermediate IR of course, ie. say, if your IR is already continuation-based.

It'll be interesting to see how this plays out on the JVM though.


I was about to say we are essentially talking ourselves back into green threads but I think you may be right about CPS.

However, the paradox you have to manage here is this: if you make this sequential/parallel determination pretty high up in the code so that there is a lot of sequential code right after an async part (async can call sync all it wants), then very little of your code is altered, but now you have the green threads problem - one long CPU-bound task that doesn’t yield.

At least with Rust, if you’ve partitioned your data properly then there can be several threads of execution running tasks on unrelated data, but in Javascript you will be punished for trying to consolidate the async bits in this fashion.


4. Pass the incoming continuation as an explicit parameter to the async function and, as long as it never escapes the function (which is equivalent to the async keyword case) or (recursively[1]) any inline child function, the CPS transformation can be performed without full whole program analysis. If the continuation escapes beyond the ability of the compiler to track it, fall back to 3. Annotations can be used to guarantee the 0 overhead optimization.

[1] true non-tail recursion is a problem of course, but it is ok for the compiler to give up if it can't statically prove that the recursion depth is less than a documented maximum.

edit: rewording


It sounds like you're suggesting programming in explicit CPS/callback style, which is doable, but doesn't match the goal of the initial post which was to program in direct style and let the callee determine whether it should execute async or sync.


Oh, not at all, I want the compiler to do the CPS transform for me (same as for yield/await) and then hand me the incoming continuation as a first class lexically scoped parameter.

For example: convert an internal iterator to an external one (in pseudo C++, yes, yes, I know):

  // This is a bog standard template function. Cont can be anything 
  // here as long as it is callable. It literally does the same thing 
  // as for_each
  template<class Cont, class Iter>
  Cont inside_out(Iter begin, Iter end, Cont yield){
     // as long as yield lifetime is confined to the scope of 
     // inside_out; when Cont 
     // ends up being some compiler internal (possibly unique) magic  
     // type it should be possible to CPS convert both inside_out and 
     // for_each. Both rust and C++ monomorphize templates anyway so 
     // instantiating dedicated CPS variant is not a problem.
     std::for_each(begin, end, [&](auto&& x) { yield(x); });

     // for various reasons we need to return the continuation here. A 
     // proper language would tail call to it.
     return yield; 
  }

  ...
  // this is basically call/cc. It uses magic to get the current 
  // continuation, then  invokes its first argument with it and the 
  // remaining arguments. The current function need to be 
  // Note: template functions are not first class in 
  // C++, but we take some liberty here.
  auto cont = continuationify(inside_out, my_vec.begin(), y_vec.end());

  while(not cont.done())
     std::cout << cont();
if inside_out was not templated, but instead Cont was type erased (a trait object in rust parlance), then continuatinify would need to allocate a dedicated stack and fall back to good old userspace context switching (the compiler is welcome to pierce through the type erasure layer and optimize anyway, but it would not be a guaranteed optimization).

Rust is close enough to C++ that it would work in the same way. I've been planning to write a paper for the C++ committee to add fire to the current coroutine controversy and proposing something like the above (which would unify stackless and stackful coroutines).


I think they're suggesting programming in implicit CPS/callback style. Eg:

  int bar(int i) { return i+1; }
  /* bar takes a implicit return address in [esp+0] */
  noreturn foo((*ra)(int),int x)
    {
    /* where (*)(RetType) is a return address */
    int y = bar(x); /* x86 call pushes eip */
    ra return bar(y); /* pseudo-tail-call (push ra) */
    }
  int quux(int x)
    {
    (*ret)(int); ret = (return); /* get implicit RA */
    foo(ret,x);
    } // unreachable
You'd also need to adjust the stack and/or frame pointer though, which is probably going to be the hard part.


typing "async" and "await" are themselves overhead; this contrasts with what the Go compiler abstracts away, isn't it?


Go takes approach #3 that I described.


Go has a really neat stack allocation technique that makes #3 extremely non-wasteful compared to C and Rust threads. Unfortunately Rust cannot use the same trick as is is llvm based and llvm doesn't support that technique.


> Unfortunately Rust cannot use the same trick as is is llvm based and llvm doesn't support that technique.

This is a bit mistaken; ancient versions of Rust (but not so ancient that they weren't using LLVM) were, like Go, everything-is-implicitly-async, and Rust used the same strategy as Go to manage stacks (to the extent that the old language reference listed Go in its (rather long) list of precursors, specifically calling out its split stack approach).

The ultimate reason that Rust switched away from Go's approach is for interoperability with C. If your stacks are the size of C stacks, and if your language doesn't feature a pervasive runtime, then interoperability with C is nearly trivial, and this was something that Rust dearly desired (and has benefited greatly from, IMO).

Ultimately one should not look at Rust's chosen approach and assume that it represents some grand rebuke of alternative approaches to asynchronicity. If you have different constraints, then a Go-style approach is lovely. For a language at the level of Rust, different tradeoffs may dominate.


> If your stacks are the size of C stacks, and if your language doesn't feature a pervasive runtime, then interoperability with C is nearly trivial

in .NET stacks are different, the runtime is relatively large, yet interoperability with C is nearly trivial. From my PoV at least, I never worked on compilers, only used them a lot.


Interesting, though note that when I say "trivial" I'm not referring to the amount of effort it takes on behalf of the programmer using the feature, but the amount of work that needs to be done (and hence CPU time that needs to be spent) when crossing the boundary from one to another and back again (which is often invisible in the source and implicitly inserted by the compiler/runtime). In Rust there is no cost to traversing this boundary (the only hypothetical cost being that inlining is more difficult (though still technically possible, miraculously) across language boundaries).


> the amount of work that needs to be done (and hence CPU time that needs to be spent) when crossing the boundary from one to another and back again

That amount is really small for .NET. See this doc https://docs.microsoft.com/en-us/cpp/dotnet/calling-native-f... it says “PInvoke has an overhead of between 10 and 30 x86 instructions per call.” That’s barely measurable. Close to the cost of a mispredicted branch. 4x cheaper than a single cache miss.

That’s if you do it correctly, i.e. only marshal value types or arrays/pointers of them, don’t use custom marshallers, etc. Also it’s important to specify correct attributes. To call `int func(const sStruct&)` C++ function, you should specify `[In] ref sStruct` in C#.


It's easy to call C from .NET, but is it easy to call .NET from C? In a cross-platform manner?


Yes, and yes.

Declare a delegate in C#, apply [UnmanagedFunctionPointer] attribute specifying calling convention (on Linux you’ll likely want CallingConvention.Cdecl), pass to native code specifying [MarshalAs( UnmanagedType.FunctionPtr )] in the prototype of the DLL/SO function which accepts that pointer.

This is even more convenient than doing that in C. In C, function pointers are stateless, typical design pattern is pass accompanying `void* context` for the state. Not needed in C#, that delegate can capture whatever it wants, the runtime will do it’s marshalling magic, generating a native function and somehow associating it with the captured state.

The only caveat is lifetime. Function pointers can’t be retained by native code, they’re too simple and don’t have a ref.counter. You must ensure the .NET delegate is not garbage collected while it’s referenced by the native code, or the app will crash.


Go still needs to fallback to heavy stacks for c-ffi. In fact that’s the main reason rust dropped stackful coroutines, because you lose zero-cost ffi to c.


Do you have any links or details about this technique? It sounds interesting and I'd like to learn more.


This is all just down to a poor choice of words, and maybe syntax.

The choice of `async`-vs-not is not the one you describe. What we label with `async` is a particular compilation style that makes the function interruptible in userspace, in exchange for making synchronous and recursive execution a bit more complicated. This style is important because it gives you event loop-style concurrency without the performance costs of threads (kernel or userspace/green/etc).

However, this compilation style still leaves the choice you describe up to the caller. Most languages seem to reverse the default choice syntactically, but you can still synchronously block on a call to an async function or run it concurrently with something else. It just so happens to be useful primarily for doing the latter, so it gets lumped in with it. (And we get unfortunate misunderstandings like "What Color is Your Function?" that miss this point.)

As an example of a language that doesn't reverse the defaults, look at Kotlin's `suspend fun`s. While they still have `async`-like callee annotations to control the compilation style, a simple function call behaves the same regardless of the callee, and you instead use various `spawn` or CSP-like APIs to get concurrency.

If changing the keyword and making the defaults match threads/CSP isn't enough, perhaps viewing the annotation as part of an effect system would help? The thing being tracked here is "this function can suspend itself mid-execution," and a good effect system even functions be polymorphic over things like this. For example, an effect-polymorphic function passed a closure or interface can automatically take on the effects of its callees, simplifying the program somewhat if you're faced with viral async-ness.


I can confirm there are at least 2 of us. I suspect a few more given previous discussions on the topic, e.g. [0].

Steve Klabnick provided some useful context on that thread for the Rust decision specifically. More generally though, you've nailed my primary misgiving:

> Synchronous or asynchronous is something up to the caller to the decide – not to the function itself.

Erlang/Elixir and Go place the decision with the caller, not the callee. That just seems much more sensible.

An alternative is perhaps to see async/await as dataflow going through the awkward teenager stage. Maybe one day we'll wake up and languages in general will have fully embraced dataflow variables as first order constructs.

[0] https://news.ycombinator.com/item?id=20400991


In go (and anything else with green threads), you pay the price for not putting await in front of stuff by introducing a whole new semantic concept of different threads which must now communicate with each other with other specialized data structures etc. Multithreading is useful when you will actually be using the CPU’s full capacity, but is an enormous extra abstraction if not. With async/await you just have to introduce a little extra syntactical clutter.


You need synchronisation primitives with async/await too. If you make every function async then you basically have the same as 1:N green threads. If you have pre-emptive threads or true multithreading with a N:M model then you do need to be more careful with synchronisation, but in principle this is orthogonal to async/await vs green threads.


Can't a compiler easily spot most cases and convert code from multithreaded to async style under the hood, at least in idiomatic Go?


Count me as third.

I dislike how this stuff "infect" all your code calls:

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

And the problem is rust is that we already have other stuff that infest everything: Borrows, mutability, ownership...


> And the problem is rust is that we already have other stuff that infest everything: Borrows, mutability, ownership...

I don't think anyone disagrees that the annotation of these concepts play a very heavy role in the type system and cognitive burden of writing code. But there are really three alternatives you can do here:

1. Do nothing, and rely on programmers to manually remember how to do all the necessary bookkeeping themselves. This is better known as "C", and the sheer amount of CVEs and other problems in code written in C is ample evidence that this approach just doesn't work.

2. Make the runtime do the automatic programming. JS is the exemplar of the pattern here. But this approach requires the VM to do garbage collection itself (which adds uncertain hidden overhead), generate code to handle arbitrary shapes (which can result in hidden performance cliffs). And heavily dynamic languages also turn compiler errors (such as fat-fingering a variable name) into runtime errors that can be accidentally ignored.

3. Shift the annotation into a mandatory part of the language, as Rust does. It makes the cognitive burden of writing the code higher, it requires more coding, but the resulting code tends to have the highest performance and lowest bugs of any of these approaches.

The tradeoff you have to make is between "no safety", "compile-time safety", and "runtime safety"; the costs you have to weigh are the costs of testing to uncover bugs, the costs of programmer cognitive and annotation burden, and the (often hidden!) costs of the runtime to dynamically enforce the conditions.

Rust has chosen to live in the camp of low implicit runtime cost and high compile-time safety, and it is really to pay the price for that with extra annotation burden. You may disagree with that choice, but it is a conscious choice that has been made.


> 3. Shift the annotation into a mandatory part of the language, as Rust does.

This is what I want. I just not like the specific implementation of awaits. I'm not much against it: i could live with it (I have used C#/F#/JS that is similar).

The thing is that still split the world even if you don't want it.

I think CSP/Actors have lesser cognitive load but of course I don't know how make them work without a runtime..


> I dislike how this stuff "infect" all your code calls

Unlike in JS, it really doesn't. In Rust, at any point you have the option to run a Future returned from an async function to completion in a sync function (which blocks the thread) and stop the spread of async.


I don't like the "what color is your function" post, because it presents Go's M:N solution as something new, when in fact it's just a user-mode implementation of threads just like NGPT was back in the early aughts. In fact, we have M:N in Rust too. It's not sufficient for people's performance needs, which is why async I/O exists. Either 1:1 threads or mioco might be sufficient for your performance needs; if so, you're free to use them and you won't have to worry about red or blue functions!


I re-read and don't see where Go is claimed to be "something new".


Infests sounds like it's a negative, there's always go if you'd prefer to have a GC and no generics. Those have other well known costs as time goes on


All of those languages have a runtime that enables this sort of thing. That's not the case in Rust. It's a bit of an unfair thing to knock it for, given the restrictions.


Particularly with Erlang with beam VM which can be heavy duty for smaller stuff. Which is totally fine for the use-cases and comes with tons of benefits. Ie. everything can be a async and decoupled with actor-style processes since it's such a low investment both CPU/memory wise and cognitively for the developer... with plenty of abstractions and well-documented best-practices to help you manage them (documentation and top-level support from the language creators itself is always valuable).

While also making your software more resilient to failure because error handling is easier as you can contain the code into small processes, communicate through messaging, let processes fail, and handle/restart them from the supervisor. Using the OTP supervisor model encourages you to think through those things, like error handling and the lifecycle of your code, so it doesn't impact other parts of your running app. A lot of exception handling in other languages is often bolted-on after-the-fact and isn't part of the typical design, at least for more inexperienced developers.

It's best to commit fully to either a full-featured VM or Rust/c low level OS style IMO.

The latter group can still support a more complex actor model with libraries and frameworks (ie, https://actix.rs/) which is more flexible but nothing beats having it be the 'standard' philosophy of the language and fully baked into the runtime.


Kotlin has an interesting midpoint between async/await and built-in lightweight threads. With async/await calls are async by default and if you await them they become synchronous. In Kotlin and with built-in lightweight threads this is reversed, but in Kotlin the callee still has a different function signature depending on whether it uses async internally or not.


I believe that this is how Scala does it, too. The caller provides an execution context that defines how the task will be run.


Thing of it as a different analogy. There are various chores to do around the house - some of them must be done synchronously (do the ironing - we don't want to leave the iron on). Some of them can be done asynchronously (run the washing machine). If you want to write out your tasks it might look something like this.

  LoadWasher() (sync) 
  washerRunning = RunWasher() (async)
  var cleaning = CleanKitchen()  (async)
  wetClothes = await washerRunning;
  LoadDryer(wetClothes)
  clothesToIron = await RunDryer()  // while we're awaiting we walk back up the stack and can continue cleaning the kitchen until the dryer is finished
  IronClothes(clothesToIron) // this is just happening synchronously
  await cleaning //last job to do so we need to make sure we finish!
Don't know if that helps, but I think it's an interesting analogy


That is a good example. It illustrates that async is a partial abstraction on the way to dataflow. It's not immediately obvious from your example what's really going on: whereas re-writing it as dataflow pipelines makes the intent clear:

    LoadWasher() | RunWasher() | LoadDryer() | RunDryer() | IronClothes()
    CleanKitchen()
where `|` is piping the output of one operation into the next one (as per *nix shell). The two lines are independent and so can be scheduled according to resources available.

Doing so makes the inherent dependencies trivially clear. It doesn't matter which are "long" running and which aren't: it's not possible to run the washer until it's been loaded. Using async/await conflates two things:

1. What's "long" running, for some definition of "long" (--> make it async)

2. What's dependent on what (await)

--

EDIT: fix formatting & added missing "RunDryer()" step


So you basically you'd have to rely on each of the function signatures, probably with some text editor/IDE by hovering over them, to figure out which one in the pipe list is Async? Or jump to each of them with ctags.

Then wrap it in a try/catch or pattern match the result to handle the various errors different errors...


> So you basically you'd have to rely on each of the function signatures, probably with some text editor/IDE by hovering over them, to figure out which one in the pipe list is Async? Or jump to each of them with ctags.

No, the point is you don't _need_ async. At least not as a language primitive. Instead, one expresses directly where the data dependencies lie. If an operation in a pipeline blocks, the runtime is at liberty to switch to another task. That already happens today, whether in language runtimes (e.g. Node) or the underlying OS. I don't need to tell the runtime "this operation might block". It already knows.

Instead, we've explicitly defined intent: when `RunWasher()` finishes, `LoadDryer()`. It doesn't matter if the washer takes one micro second or one hour; I can't load the dryer until it's done.

>Then wrap it in a try/catch or pattern match the result to handle the various errors different errors...

True, error handling is missing. But then it's not in the parent example either. The requirements aren't fundamentally different in either case.


Your analogy works as long as there's a machine doing the washing and drying.

If you replace the machines with people, it makes much more sense for the function definition to define what is and isn't synchronous. If you have to do all the chores yourself, just call the functions synchronously. If you have family members you can delegate some work to, you can call the functions asynchronously and wait for them to complete while you're doing your own work (or twiddling your thumbs).

Elixir allows you to do this very cleanly. https://hexdocs.pm/elixir/Task.html#content


Another problem with await is that you can't run in parallel without going down one abstraction layer. So it just add additional complexity as you need to know two layers. The hardest part with software is to not implement too many features.


My problem with this is if 'I' (or in code, the current program) actually loads the washer and cleans the kitchen, then these things run in serial.

If I do what I've always done in rust, which in use threads, I get lovely easy parallelization. Async only seems useful when your program doesn't do anything non trivial, but just dispatches to other things, as any one function that takes a long time blocks all other async functions until completion?


Yes, but you can run your blocking code on a thread which presents itself as a future:

- What is the best approach to encapsulate blocking I/O in future-rs? — https://stackoverflow.com/q/41932137/155423


The problem is that you don't always know beforehand whether you'd like to perform a task synchronously or not.


It’s easy to run an async function synchronously; just drop the `await` keyword and wait on the returned future.


And so all library functions should be async by default? And if everything is async, then shouldn't synchronous programming require the extra keyword instead of the other way around?


No. A function that makes no async calls has no reason to be marked async.


And if someone wants to add an async call later, all callers need to change too.


Only if the function "leaks" the async-ness instead of containing it (that is, waiting on any async calls synchronously). But yes, making a function async is a backward-incompatible change, just like manually changing the return type from `T` to `Future<T>`.


Ok let me phrase it differently. If you have some library function that does some IO (for example) and you want to make it as useful as possible, would you declare it async or not? If you make it blocking, then you'd ruin any opportunity to have it called from async functions (because they don't want to be blocked). Therefore, you'd make it async. So that means that all IO library routines will become async, and possibly a lot of others too. Wouldn't it make more sense to have async be the default?


But when all those IO functions are rewritten to be async, their public names can be different. The sync versions needn't change names, so none of the callers would need to change. Nor would the sync versions need to duplicate code, because they can just be thin wrappers that call and wait on their async counterparts. (Note that this wouldn't be possible in JS.)


and it can change. Let's say your bandwidth goes from 1GbE to 10GbE and now your bottleneck has changed. You may want to refactor which task is async.


Wouldn't it be desirable to replace async with sync, then?

I'd rather dirty the signatures of blocking/synchronous methods then of the rest.


Sync is still much more common than async. I'd rather write `await { httpCall }` than `blockOn { 1 + 1 }`


That would make a lot of sense, but wouldn't be very practical to add in to an existing code base.


Unlike JS, most languages, including Rust, are not async by default, so this would not make sense.


How is JS async by default?


When people say this, they usually mean "a significant amount of things JavaScript is used for are asynchronous, and IO is virtually always asynchronous," rather than something more literal. The language has asynchronicity deep in its bones, even if it's not entirely made out of it.


That's 'lying' though, async/await is a great way to know which of your functions perform i/o and which do not.

Namely in platforms like Node the convention is to not do synchronous I/O and as a communication tool - everything that doesn't use async/await is promised to never block or perform I/O.

You might claim it's a lower level of abstraction but it makes concurrency a _lot_ simpler and it's why often when I write go I have to write 35 lines for something that would take me 5 in C#, Python, JavaScript or now Rust.


Thanks, that's interesting point. I see how it might be useful, but it's still looks as a minor optimization that doesn't outweigh the drawbacks of the model.

How do you deal with a situations when you have non-async/await code, and then you need to call just one function, which happened to be "async", and you can't do until you mark all your functions async as well? I find myself in this situation too often with Dart/Flutter, and it's especially embarrassing experience as it happens usually with a simple parts of code, where I don't need concurrency at all (like, get local directory name and store file to it). This "forced refactoring" also hits language design corners like "you can't call async functions" in constructor or `iniState()` and I end up writing async wrappers and other helpers functions just to make the whole thing work.

> it makes concurrency a _lot_ simpler

I find people put different meaning in the word "simpler". Do you mean that code requires less lines/chars to write or that it's easier to read and understand (those are totally different properties, often conflicting)?


I hate Node's model of async concurrency. I need one thing to happen, then another depending on the result of the first, and so on, way more often than I can let several things happen at once or in any order. Probably something like 100x as often. Even when I can have multiple pieces of logic executed at once or in any order, it's usually just a couple, and internally they usually comprise more of the same dependently-ordered execution. If I can really kick off a ton of something at once it's usually one function/operation, which again... consists, internally, of a bunch more stuff that needs to happen in a certain order.

It'd be way more convenient if Node just assumed it couldn't carry on with the next operation until the current one was done, even if the current operation is async I/O—feel free to go off and process something else, just no more of my code—unless instructed otherwise. IOW, caller decides, yes. I 100% do not understand the appeal of a system where you have to specify that you do want the next instruction to wait on the result of the previous one. "But you'll block node's single thread during your I/O!" no, it can go do something else, just no more of this particular line of logic in my code unless I specifically tell it otherwise, thanks.

Caller decides is both easier to follow and more useful.


Sounds like you might prefer Erlang or Go.

But to be clear, "it'll block Node's single thread!" is not the reason for explicit await. The reason for explicit await is to guarantee that after calling a function foo(), only the state that foo() changes could have changed. Whereas after awaiting on an async function bar(), or any other promise, literally any state-mutating code in the entire application might have run.

Obviously plenty of languages have this problem and deal with it, usually with locks like in Go and Java, or by banning shared mutable state entirely and only allowing message-passing for communication between concurrent "lines of logic" like in Erlang. (As far as I understand Go basically tries to get the best of both worlds by strongly encouraging message-passing, with locks intended only for advanced situations like custom concurrent data structures.)

But there's no way for Node to change to either of those models without breaking every single nontrivial piece of code ever written in it. And apparently Rust intentionally chose not to go with those models because they require a runtime and make compatibility with C very difficult, well-known advantages over Go.


> How do you deal with a situations when you have non-async/await code, and then you need to call just one function, which happened to be "async"

This depends on what do you want to happen. Do you want to block on the one async function, or do you want to turn your current function into async and propagate the asyncness to your caller?

Both options are available to you in Rust.


I think the people criticizing it are, like me, coming from other languages where blocking on async doesn't make your function sync. The asyncness still ends up propagating to the caller function in such languages.


Thank you for putting it explicitly, everything here suddenly makes so much more sense.

But yes, in Rust async is not a plague that irredeemably taints everything. You can choose to use anything that is async synchronously if you choose to, just by calling .poll() on the future at the callsite.


> How do you deal with a situations when you have non-async/await code, and then you need to call just one function, which happened to be "async", and you can't do until you mark all your functions async as well?

Just call the poll function on your Future.

Just because Javascript sucks doesn't mean the similar feature in Rust does too. :)


In C# you can't await an async in non-async function, but you can block until it's done. There is no drawback to having all code async.


There is a drawback: it pollutes the signatures of each asynchronous method and the signatures of the consumers.

Each method now returns a Task, and each method name ought to have a suffix of 'Async'. It sucks that changing this will require all consumers to also modify their signatures as well. But I understand why it is necessary, the compiler needs to mark each 'step' in the asynchronous function to create a kind of state machine.


But it doesn't pollute the signatures of the consumers, that's the whole point of this subthread.

If you don't want/need to expose your async-ness, you just synchronously block on your async callees instead of awaiting them.


I think this article justifies 'simpler' as more than just "characters to write". Rust is a fairly different language in that it isn't GC'd, and the async/await sugar really alleviates a lot of pain around that (among other things).

As for calling async from sync, I imagine you just create a runtime and block on it.


> everything that doesn't use async/await is promised to never block or perform I/O

I don't think that's true. See fs.readFileSync for example. You could say that's an exceptional case (which I think it is), but it means that there's no "promise" that functions without async/await don't perform I/O.


Right, there is always an escape hatch, this is also true for haskell with unsafePerformIO - because of practicality.

That's a good thing :]


a) this function is clearly marked as sync b) the question of why does it exist if the async version if available is beyond me


rom a practical perspective, it's much easier to deal with `fs.readFileSync` in top-level code than to dive face-first into callbacks. (Remember, `fs.readFile` doesn't return a `Promise`.)

If `fs` was promise-aware and top-level await existed, it would be a different conversation. As-is, for writing a one-off script, avoiding callback hell is pretty nice.


Top-level await is still a problem but a Promise version of the `fs` API has been available since Node v10.

https://nodejs.org/api/fs.html#fs_fspromises_readfile_path_o...


TIL - never looked. (Most of our stuff is stuck on 8.x and I've got some work to do to upgrade us; never looked at the changes!) Thanks for the pointer.


Agreed. I've used it from time to time for writing simple scripts and it does make things much easier. You don't really have to worry about a CLI script blocking.


If I call you on the phone, I expect a synchronous response.

If I text you, I expect an asynchronous response. It's ok if you get back to me 3 hours later (well, context-dependent).

If I call you, ask you a question, and then you wait 3 hours before giving me a response, I've been sitting there blocked for 3 hours, unable to get any more work done, because I asked for a synchronous response and got an asynchronous one. Even worse, if you try and give me the response over text, I won't see it because I have the phone pressed to my ear and now I'm blocked forever.

---

The caller and callee have to agree on whether the communication is synchronous or asynchronous. Asking for synchronous and getting asynchronous doesn't work. Asking for asynchronous and getting synchronous kind of works, but that's not truly synchronous, that's just asynchronous with zero delay before getting the response. Or even worse, asking for asynchronous (and giving you a completion handler to fire) and you fire it synchronously before returning control to me, that way lies madness. Don't do that.


You aren't the only one. In Kotlin, it's possible to use async/await without modifying your function signatures. Like this:

  val result = runBlocking {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    one.await() + two.await()
  }
https://kotlinlang.org/docs/reference/coroutines/composing-s...

EDIT: actually this was wishful thinking, since both functions would block the main thread. The functions need to be modified to "suspend" for this to be truly async.

My ideal language would be something like Kotlin suspend semantics, except ALL functions were implicitly "suspend" functions. If that's even possible...


Ditto in Rust.

    let result = executor::block_on({
        let one = async { do_something_useful_one() };
        let two = async { do_something_useful_two() };
        one.await + two.await
    });
See examples in:

- What is the purpose of async/await in Rust? — https://stackoverflow.com/a/52835926/155423

And why this might be a bad idea in:

- What is the best approach to encapsulate blocking I/O in future-rs? — https://stackoverflow.com/q/41932137/155423


This code block doesn't actually work the way the Kotlin one does. Async functions are mere pull-based state machines, so both `one` and `two` will compute serially, without concurrency.

What you need is:

    use futures::join;

    let result = executor::block_on({
        let one = async { do_something_useful_one() };
        let two = async { do_something_useful_two() };
        let (one, two) = join!(one, two);
        one + two
    }); 
I feel like this might be a common stumbling block and hope there will be a Clippy check for this or something.


Ah, thank you. I wasn't aware that the Kotlin syntax automatically raced `one` and `two`. I'm not sure how I feel about that, but I haven't thought deeply about it. My gut is kinda "meh".

In the spirit of getting it right, your code is missing an `await` and `join` is back to a function in alpha18. Here's the currently working (this time tested!) code:

    #![feature(async_await)]
    
    use futures::{executor, future}; // 0.3.0-alpha.18
    
    fn main() {
        let result = executor::block_on(async {
            let one = async { do_something_useful_one() };
            let two = async { do_something_useful_two() };
            let (one, two) = future::join(one, two).await;
            one + two
        });
    
        println!("{}", result);
    }
    
    fn do_something_useful_one() -> i32 { 1 }
    fn do_something_useful_two() -> i32 { 2 }


It doesn't automatically race. The "async" block in Kotlin is an equivalent to "spawn" in Rust, and launches everything in it as a child task. However compared to Rusts spawn Kotlin makes use of structured concurrency - which means parent tasks wait for child tasks and if you cancel a parent task the child task will get notified. So it's meaning is definitely closer to the join() based solution you documented.

The sequential but still async version in Kotlin is simply

     val result = runBlocking {
         doSomethingUsefulOne() + doSomethingUsefulTwo()
     }
No awaits necessary


Oh cool. And does that actually work? That's great.

And the syntax is surprisingly similar, especially considering we are supposedly comparing a low level language to a high level one.


Yes, one big push in Rust is that you get to have your high-level syntax with your low-level performance.

See the sibling comment [1] where I actually tested and ran the code, finally. ;-). I was unaware that Kotlin would race two `async` blocks, so we had to use the `join` combinator here.

[1]: https://news.ycombinator.com/item?id=20682191


I am a huge Rust fan, but couldn't really articulate why async/await seemed so complicated. You have nailed it right on the head!

This is a exactly how I feel as well and I write Rust code every day.

But no matter, I will suck it up. The language is evolving and I think async/await has, seemingly, been one of the biggest divisive feature the community has faced. I will still use as there are so many other benefits this language brings.


>I am a huge Rust fan, but couldn't really articulate why async/await seemed so complicated.

Perhaps because you hadn't had the displeasure to work with callbacks, or promises? Which async/await greatly simplifies.


> Which async/await greatly simplifies.

That's probably the real reason why so many people are so happy and vocal about "simplicity" of async/await – after experiencing concurrency only with callbacks in JS, async/await looks like a blessing.


True, but at least there is no magic in using callbacks. It is clear what is going on.


> True, but at least there is no magic in using callbacks. It is clear what is going on.

So, tell me, which thread did that callback run in? And what happens if I need to grab a lock in the callback?

That's "magic".


There's no magic in async/await either. It's all just sugar.


Sugar is magic. I still don't know what happens behind an async and await and no tutorial really explains that to you.


https://github.com/rust-lang/async-book will be that resource, though it's not complete yet. It's important for systems languages to be able to see how stuff is implemented and reason about it, so we're committed to making it as non-magic as possible.


I disagree, magic is magic. If there's sometime done by the compiler that is impossible to do yourself in the language-- that's magic. Everything async/await is doing you could do by yourself. There's no magic in the Future trait, or any part of Rust's async solution.

You can't call it magic just because you haven't read what it desugars into. Otherwise anything you don't know could be called "magic".


Well, there's nothing you couldn't write yourself, sure... but async/await does generate unsafe code in some cases, so it is at least somewhat magical that you can do it in safe code. Since async/await has unsafe code that is really fiddly to get right otherwise (c.f. the rules for `Pin`), I think this is a pretty good tradeoff and a great use of magic.


The OP explains what's happening with the examples it provides. Was that explanation not convincing?


Well, there's a whole bunch of magic, from virtual memory, to linking, to branch predictions, all the way to microcode, that's not clear, so there's that.

(In computing there's not magic. There are abstractions, and it's not that always less is better).


You're missing the point, but you probably know that anyway.


No, I think you're missing the point:

To validly complain about an abstraction the abstraction should make reading the code (or other aspects, e.g. performance) worse.

Merely complaining that "it hides things" and that you "have to learn it to know what it does" is not a valid complaint.

That's literally what abstractions are supposed to do: hide things and introduce new things to learn (the abstraction.

So you can't use the fact than an abstraction like async/await hides things ("magic"), or introduces something new to learn, as an argument against it (unless you're against all abstractions, but this train has long sailed, and the question whether abstractions can be good is settled: yes, they can).

So, the whole point to judge an abstraction is whether the new thing it introduces makes things easier _after_ having learn it or not.

So far your arguments were just that it hides things (magic) and that callbacks made what happened clear (so, again, the hiding aspect).


If you mean clear as in readable, I disagree. async/await greatly increases readability of code. remember callback hell? remember callbacks for exception handling? they all dissolve into code that reads as synchronous.


No I definitely didn't mean clear as in readable. More as in, I know how this working and why it's async.


Yes, but after sitting down 20 minutes or a day and learning how async/await works, for the rest of your life reading async/await code is clearer than seeing nested callbacks upon callbacks.

The same way code using functions is easier to read than looking at the exact same code expanded in 20 places in your program, even if a function call also has "magic" (passing the arguments on the stack, replicating the behavior inside the function as it was written in the place of invocation, sometimes inlining, returning, moving the stack pointer again at the end, recursion, and so on).

You learn once how function works and that's it. More compiler behind the scenes work ("magic") than writing the same lines again and again, but easier to read.


Just don't use it. I presume you could, in rust, use an actor model like Actix and do async/await like Elixir does -> from the calling thread spawn a new actor that runs the async task, which sends a message back with the result on completion; parent thread does its thing, and then blocks on receipt of the response + a reference token.


I think of async to mean "When you call me I won't actually do anything right now, I'll just prepare the data structures to help remember what needs to get done, and return those structures to you, which you can then hand over to an event loop (executor) at your leisure". That return type? That's not actually what is getting returned right now when you call the function... that's what the future returned will eventually resolve to. That level of unexpectedness/magic is hard for me to stomach but in this case I'm ok because the feature provided and the ergonomics are so very good.

I think of await to mean "When this code is combined with all the other code that needs to be executed asynchronously on the event loop, this code will not be able to progress until this thing we are waiting on finishes. The event loop can therefore use this hint to schedule things and keep the processors busy."

I think I built up a good understanding of how this kind of a system works with futures 0.1 and tokio. It took some time but it all clicked together for me. But as for async/await as language features I'm satisfied to just let it be magic. I don't care to look under the covers at this point. I know it's similar to how futures worked, and I trust the rust team weaved it together well.


I agree. I raised the topic recently on HN here [1], but didn't get much of a response.

One of the problems with async is that it's viral. Make a function async and your entire call graph has to be made async, unless they handle the future manually without await. Async introduces a new "colour" to the language.

I absolutely agree that asynchronicity should be the provenance of the caller. If you look at typical code in languages with async/await such as JavaScript and C#, awaiting is the common case. So if async causes your code to be littered with awaits anyway, it makes more sense to make awaiting the default (just like it's the default for the rest of the language) and "deferring" the exception. Call it "defer" or something.

(As an aside, I was always disappointed that Go's "go" doesn't return a future, or that indeed there's no "future" support in the standard library. Instead you have to muck about with WaitGroup and ErrGroup and channels, which introduces sequencing even in places that don't need it. Sometimes you just want to spawn N goroutines and then collect all their results in whatever order, and short-circuit then if one of them fails. The inability to forcibly terminate goroutines is another wart here, requiring Context cancellation and careful context management to get right.)

[1] https://news.ycombinator.com/item?id=20405124


> One of the problems with async is that it's viral. Make a function async and your entire call graph has to be made async

In most languages, including Rust, async functions can call synchronous functions, so this isn't entirely true.

> unless they handle the future manually without await.

At some point, the futures are always handled manually, e.g., via the run-time, or in Rust case, by scheduling on an Executor, which is nothing but a fancy name for a library that manually handles a lot of futures.

> If you look at typical code in languages with async/await such as JavaScript and C#, awaiting is the common case.

Do you have any example of code that uses await more than synchronous functions ?

> So if async causes your code to be littered with awaits anyway, it makes more sense to make awaiting the default (just like it's the default for the rest of the language) and "deferring" the exception. Call it "defer" or something.

If the purpose is just to change the defaults, then you'd need `defer vec.len()` to access the length of a vector, or `defer array[i]` to index into an array, or `defer obj.foo` to access the field `foo` of an object, ... unless you want to make all of those operations `async`.

If you make await the default and add a `defer` operation that doesn't awaits, then it's hard to imagine how the resulting code could contain less `defer` annotations than `await` ones.

At that point you might choose one of the many non-zero-cost solutions to removing the async distinction, and call it a day like Go does.


I feel like you misunderstood several of my points.

First, async infects the call graph, that is, going up the stack. Not down.

Secondly, by "manually" I mean using the future API -- and_then() and so forth.

Thirdly, "defer vec.len()" doesn't make sense unless you want to execute it asynchronously and get a future. And that doesn't make sense unless you know it to be an expensive operation you want to run concurrently. I'm proposing that the hypothetical "defer" would act like Go's "go", except it would return a future.

Lastly, my point was that most code is serial and relies on awaits. For example, if you're doing a name lookup, then opening a socket, then writing to it -- three async calls that must be serialized with await because the actions depend on each other. Not awaiting is needed when you intend to use the future for some purpose, such as adding to a queue or putting in a map, or perhaps you're doing N connections that you put in a vector to ask the runtime to wait for all of them (does Rust's await syntax support awaiting multiple futures?). But most high-level "user" code doesn't juggle futures, it just awaits whenever it needs a result. From a language-ergonomics perspective, it makes more sense to optimize for the common case.

I've never written any async/await code in Rust, but I've written plenty in JavaScript, and it's the same story.


> does Rust's await syntax support awaiting multiple futures?

No, but you use a join combinator, which itself returns a future, that you can then await.


> Do you have any example of code that uses await more than synchronous functions ?

But that was not what I was arguing. Rather, I was arguing that await is more common than capturing the raw future. Case in point: The article, where all of the "real code" snippets rely purely on await.

What I proposed was that async functions would always be automatically awaited unless invoked with "defer", because most code is expressed as a series of invocations that use the return value of the function call.

There are times you want to store the raw future as some kind of state (for example, in a GraphQL server you might "resolve" the GraphQL tree as futures), or wait for multiple futures, or similar, but those are, in my experience, the exceptions, not the norm. Most code is:

  let v1 = do1().await;
  let v2 = do2(v1).await;
  // etc.
In other words, serial.


Maybe only await functions by default that are await and normally call functions that are not?


> Make a function async and your entire call graph has to be made async, unless they handle the future manually without await.

You can "handle the future manually without await" very easily, it's just one function call. The same sort of thing is available in most other languages with async/await- you can prevent async from going up the call stack by synchronously blocking on a future instead of awaiting it.

The caller is absolutely in charge of whether something runs asynchronously. The only difference is a syntactic default- and in fact some languages even make async functions calls default to synchronous execution. For example take a look at Kotlin's `suspend fun`s.


One of the problems with async is that it's viral. Make a function async and your entire call graph has to be made async, unless they handle the future manually without await. Async introduces a new "colour" to the language.

Not in Rust. Or C#. Or any language except JavaScript.

In Rust, a synchronous function can call and block on an async function with executor::block_on(). C# has Task.Wait() and Task.Result(). Python 3 has asyncio.run(). Scala has Await.ready() and Await.result(). Kotlin has runBlocking {}.


I completely agree. In Java we're taking a different approach: https://youtu.be/r6P0_FDr53Q (my talk also explains why this option may not be available to C/C++/Rust)


Leaving it up to the caller to decide seems reasonable, but you only have control over the top level, you still have no control over how the function you're calling calls other functions.

Also, blocking on a future is trivial (just call poll).

So I would argue that async/await does a better job of leaving it up to the caller to decide: use await if you want async, use poll if you want sync.


As I've always said, you can use CSP in Rust. Just use threads and channels. They work great, and they even scale well on Linux to all but the absolute most demanding workloads. If you want a Go-like M:N scheduler, we've got that too: use mioco.

CSP as implemented in languages like Go is just threads, but with an idiosyncratic implementation.


> they even scale well on Linux to all but the absolute most demanding workloads

What about the stack size? I guess the default is about 4 MB, compared to a few kB in Erlang or Rust. This implies a virtual memory usage 100x or 1000x higher. I understand the unused virtual pages in each stack will never be mapped to physical memory, but isn't this putting a lot of pressure on the virtual memory system? I'd be curious to read a real world benchmark on this topic.


Whole-heartedly agree. I strongly believe that async/await will be acknowledged as a design-mistake a decade from now. Async should be caller and not callee controlled - it also makes the implementation simpler and reduces maintenance.


It's been many years already and async/await has proven itself to be a viable feature for shared memory languages that want to be as good at concurrency as they can. Next step is literally dropping shared memory as there is pretty much nothing that can be done better on top of shared memory than syntactically sugared higher-order asynchronous programming primitives.


I'd rather mark a function as "async" than to constantly do the mental gymnastics of whether a function is going to run synchronously or asynchronously, or even worse, always assuming that it'll run asynchronously and always have to deal with callbacks / triggers and the like just to get things to execute in order.


> Synchronous or asynchronous is something up to the caller to the decide – not to the function itself.

This doesn't work with Rusts implementation, because the functions have slightly different semantics and limitations.

E.g. an async function can't be recursive, since that would lead to a Future type of infinite size.

Async functions also don't necessarily run to completion like normal functions. They can instead also return at any .await point, which are implicit cancellation points. That means if someone just adds an async modifier to a function and some .await calls to methods inside it the result might not be correct anymore, since not all code in the function is guaranteed to be run anymore.

The cancellation mechanism was an explicit choice of the design - it would probably have been possible too to make async functions always run to completion and to thereby avoid the semantic difference.


Your thought that it's the caller makes some sense. Think of the OS level call to read a file, you can either ask for it to block until end of line, or just give you what it has now, based on a flag.

But "wait until there's eol, than call this callback" or its cousin the future which is "when complete, read here to find the callback(s) to call" which let's you fill in the callback after the call, that's inherently an async call. Same way sending a letter forces you to wait. Sure you could stand by the mailbox until the reply arrives, but that's just converting the async call to synchronous by waiting.

Though I don't really see how futures are that different from a cps, especially if you view them as chaining .then calls, which are exactly continuations. Await just let's you drop some syntax, and unify scopes.


No, a synchronous procedure is a procedure whose minimum completion time depends solely on the speed of the CPU and memory system (and lock contention, in some models), while an asynchronous procedure is a procedure whose completion time also depends on external events.

In other words, synchronous procedures are essentially locally CPU-bound, while asynchronous procedures are bound by I/O, network or IO/network/CPU of remote servers.

Async functions are the most efficient way to support asynchronous procedures, while non-blocking non-async functions are the most efficient way to support synchronous procedures.

There are also adapters to turn synchronous function into asynchronous ones, and to turn asynchronous functions into blocking non-async functions, although they should only be used when unavoidable, especiallly the latter.


This is an idiosyncratic (most would say incorrect) definition of sync and async.


I totally agree with you, however the alternatives to Async/Await are just way more wierder IMHO. Promises, Futures and what not. I am not very familiar with CSP based concurrency but Async-await strikes a good compromise to me. I just can't deal with this Promise/Future/Callback hell when it comes to managing multiple asyncrhonous or dependent asysnchronous calls. I think Chris Lattner does a way better job at explaining this -

https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9...


> I totally agree with you, however the alternatives to Async/Await are just way more wierder IMHO.

It’s not, however you do need the entire runtime to be in on it. That’s what Erlang, Go or gevent (for python) do: normal concurrency is in user land and normal IO uses « async ». The code itself looks / is synchronous, it just multiplexes in userland before doing so on OS threads.


> I am not very familiar with CSP based concurrency

I strongly recommend to check it out. I was absolutely mind-blown when saw this talk (without prior knowledge about Go), and that changed my life quite literally.

https://www.youtube.com/watch?v=QDDwwePbDtw

I did a few concurrency related projects (see "Visualizing concurrency in Go" https://divan.dev/posts/go_concurrency_visualize/, for example), and it's way more easier to reason about and work with than any other concurrency approaches I've seen so far.



That's a rant about callbacks in old style node.js and is completely unapplicable to Rust. More: https://news.ycombinator.com/item?id=20676641


Fibers are way better than async. Go has them and Java is working on it with project loom. It's a slight performance disadvantage (maybe 2%) but light-years easier to work with.

Languages with fibers figure ou execution suspend points and physical core assignment for you and abstract it all away. So physically they're doing the same thing as async, just without all the cruft.

Rust decided not to go with fibers to avoid having a runtime. I still disagree with this because they already do reference counting, not every worthwhile abstraction is zero cost.


> Rust decided not to go with fibers to avoid having a runtime. I still disagree with this because they already do reference counting, not every worthwhile abstraction is zero cost.

Refcounting is a library feature, does not require a runtime, and has no impact on code not using it.

Fibers is a langage feature, does require a runtime, and impacts everything.

Rust actually stripped out its support for fibers as its community moved its usage downwards the stack. In much the same way it stripped out « core » support for GC/RC (the @ sigil) or internal (smalltalk/ruby-style) iteration.


The difference with reference counting is that you only get it when you actually use it, but a runtime included by default does not have this property. And having an "opt-out runtime" doesn't fit the bill either; we tried that too.

(I'd also be very skeptical of the 2% figure; where did you get that from?)

Remember, "zero cost" means "zero additional cost", everything has a cost.


2% is from Quasar, a Java fiber library. Some simple benchmark compared it to async code.

Now that rust has async, couldn't they make fibers an opt-in replacement for threads? Your libraries use async, but you can handle those calls with fibers. Fiber support is compiled into your code but not the libraries


Interesting, I wonder what the details look like. We saw fairly big differences in Rust.

> Your libraries use async, but you can handle those calls with fibers. Fiber support is compiled into your code but not the libraries

We tried this! Making the attempt is actually one of the biggest reasons that we decided to remove green threads from Rust; it brought in tons of disadvantages and no real advantages.


I understand why you didn't, but async brings a whole new opportunity to try! At least in library form.

Fibers are great because you can pretend they're threads. Everyone knows threads. Async is a legit PITA even if you're familiar with it. And the local state stored for a fake thread is often useful. When doing async I find myself frequently building hacky hashmaps to hold local variables values. websocket code is a good example of worse-case. 20k ongoing connections held by async (1 thread per core). It's a nightmare in everything except Vert.X Sync (fibers in Java) or maybe Golang and Erlang.

With fibers you get to keep all your local variables and "pretend" threads actually exist. It's a huge boon for productivity in the few languages where it's possible


Perhaps it's because you can easily bolt on CSP to the languages where it's popular? JS is single threaded, and Rust and C# don't have green threads.

I consider Rust to be an antithesis to your remarks because it has realoy good tools to prevent concurrency issues and yet still wants it.

I think on a deeper level it's because there are times where the thread isn't really doing anything and is instead waiting, and that time could be better apent doing something else. That requires sync, either with callbacks, promises or sync/await.



That's a Javascript rant from 2015, and almost completely unapplicable to Rust. The rust designers were well aware of the disadvantages of various async implementations and have learned some lessons.

First he starts with a rant about the color of a function. In that sense, all typed languages have a color, aka the type of their return parameters. Sure, there a advantages to dynamic languages, but they're going out of fashion now for some good reasons.

Then he lists 5 more specific drawbacks of async code in 2015 Javascript 2015. All 5 aren't issues in Rust.

#1. Every function has a color: that's how typed languages work

#2. The way you call a function depends on its color: the rust compiler doesn't let you get this wrong and provides cut and paste corrections when you do

#3. You can only call a red function from within another red function: Use Future#poll

#4. Red functions are more painful to call: This is in reference to callback syntax in old node.js code and is fixed by the await sugar even in modern Javascript

#5. Some core library functions are red: The clause "that we are unable to write ourselves" is untrue in Rust; you should never have a need to drop down into C to write anything. And this is really just a variant of #3; if it's trivial to convert async to sync this wouldn't be an issue.


Well, practically it is for the caller to decide, you can `await` on a function (use it as if it were sync), or don't, grab it's `Future<something>` result and work with that (async).

Now `async`, you could see it as an implementation detail that you need in order to get much better performance...


Threaded IO is a low level thing there because of overhead caused by OS threads and Rust is a low level language with abstractions to deal with such low level interfaces. Async/await is better than callback passing - looks like normal call flow - but you need to know what's happening underneath. Zero cost abstractions. If you understand the implementation details it makes perfect sense - otherwise you can just keep doing the old way (callback pyramids or blocking IO)


If you don't have async markings, you can't know where in your code you have concurrency and this would require you to use synchronization primitives. For shared memory languages this means all that error-prone shared memory multithreading, like in Go [1].

[1] https://songlh.github.io/paper/go-study.pdf


But a synchronous Rust function can still do threading, which makes your point moot.


It's not moot, there is no point to do threading from within asynchronous programming primitives.


Your original comment implies that "async"-marked functions are the only functions that have concurrency ("If you don't have async markings, you can't know where in your code you have concurrency"), which isn't true. You can have threading in any function. So marking a function async makes no difference.


No, think of it as a convention or a framework. While it's ok to wait for asynchronous code to finish in your synchronous code, it's not ok to do threading from within your asynchronous primitives, so you wouldn't need threading synchronization primitives.


You can run an async routine synchronously if you use the correct runtime, or block while polling until the function succeeds... "async" functions are functions that can be suspended and turned into an opaque thunk that you can pass around as Rust data, and "synchronous" functions aren't. If you like, you can think of async functions as being functions that can run either synchronously or asynchronously.

You can't (in safe Rust) easily replicate what async does for you because it understands how to handle borrows across yield points, which turn into self borrows when you turn the stack into a concrete object in Rust. That's not really an issue in garbage collected languages so it's not quite as much of a necessity to have a specific keyword, but it's still inconvenient in most languages to write everything in CPS or use combinators to acquire thunks.

As for why you would want a function that's not asynchronous... well, various reasons, but one is performance. Creating a thunk and then sending it to a runtime which decides what to do with it (or polling) usually has some overhead compared to linearly calling a function on the stack.

Another reason is that, in Rust, top-level asynchronous functions normally need to be quite conservative with what they own if you want to use them with an actual asynchronous runtime--many of them like to send the thunks between threads in a static thread pool, which limits you to thread safe constructs and owned (or static-borrowed) data. As a result, even if there was no overhead for using an asynchronous function synchronously, you would still in practice have to either sacrifice performance and generality by keeping your data owned and thread-safe (to make things work with thread pools), or embrace all the usual stuff you would do in synchronous code (like borrow things from a previous stack frame) and lose the ability to use your async function at the top level in most existing asynchronous runtimes. So from that standpoint, it's not really up to the caller. This is again not really an issue in garbage collected languages that only allow references to managed, heap-allocated objects.

There are more reasons than that (being able to efficiently call out to blocking C APIs, wanting to use builtin thread locals across function calls without worrying about how the function call is going to be handled, current issues with recursion, etc.). They may be considered artifacts of the implementation or otherwise resolvable--I'm not necessarily saying they aren't--but they are reasons in practice why you want to have synchronous functions available.

So tl;dr I think most people's immediate intuitions about how async/await should just be sugar, or async should be up to the caller (i.e. all functions should be async), don't straightforwardly apply to Rust even if they are valid for most other languages.


I used to be a huge fan of async/await in the JS world, until I realized that we were tethering a very specific implementation to new syntax, without adding any clarity beyond what a coroutine and generator could do. eg `async function foo() { const a = await bar();` vs `co(function* foo() { const a = yield bar();` isn't really with adding extra syntax for.

I came in not wanting to see rs go down the same path. The idea of adding something that looks like it should be a simple value (`.await`) seemed odd to me, and I was already finding I liked raw Futures in rust better than async/await in JS already, especially because you can into a `Result` into a `Future`, which made async code already surprisingly similar to sync code.

I will say, the big thing in rust that has me liking async/await now is how it works with `?`. Rust has done a great job avoiding adding so much sugar it becomes sickly-sweet, but I've felt `?` is one of those things that has such clean semantics and simplifies an extremely common case that it's practically a necessity in my code today. `.await?` is downright beautiful.


I've written a lot of JS/TS over the last couple of years, and I've found that even though it may just be syntactic sugar, async/await is a major win in both how easy it is to now write async code, and how readable the code is.

Generators got us close to that, and it is a more generic feature. But over all the code I've written, I've only had one solid use case needing generators for something other than what's covered by async/await. Having a specific syntax that covers 95% of usages is very worth it in my opinion.


Looking at the example I gave, how is `async function` any more clear than saying `co(function`? It covers the exact same use cases with equivalent clarity.

They could've shipped the `Promise` object with a `coroutine` fn, like bluebird did, and had the same semantics without needing to reserve words and add syntax to the language. All it opens up is combining async and generators, which can be handy for iterating things like prefetched results, but that's a rare enough case not to need to be baked into the language.

Separating the coroutine would let us work beyond just promises too. Wrap around old code using error-first callbacks, or new code using observables (yield to get the next, yield* to complete). I just wrote a coroutine to give quasi-concurrency to Google apps scripts, so it can queue up the routines running `fetch` methods and instead do them in one parallel-request-making `fetchAll`. It's a much better approach for the long-term of a language. Imho 90% of the junk we deal with in old languages is stuff for a convenient implementation at the time that we don't use anymore.


>Looking at the example I gave, how is `async function` any more clear than saying `co(function`? It covers the exact same use cases with equivalent clarity.

It skips the generator part and extra wrapper. That's a simpler syntax (and thus more clarity) for what people do 99% of the time.

Clarity is not necessarily "I can see the underlying mechanism".

Generators hide their underlying implementation (in C++) too, after all.


> Generators hide their underlying implementation (in C++) too, after all.

that has been contentious to say the least.


In my opinion, the main reason to have `async` is because it doesn't require knowledge of generators. I've worked with developers who thought that `async`/`await` was too much trouble to learn and unnecessarily complicated. Promises allow us to act like nothing changed and functions return values. The trick, to me, is how do you meet that need without sacrificing these more powerful operations.


But it does require knowledge of generators. You're suspending a function during execution; that's what a generator is. We could've just named a global `async` that declares a coroutine FN on promises, and named `yield` as `await`, and the whole thing would look identical to now except for an extra paren and asterisk. You wouldn't need to understand their inner workings any more than you do with async/await now.


>But it does require knowledge of generators. You're suspending a function during execution; that's what a generator is.

That's like saying "Ordering from Amazon does require knowledge of driving a van. That's how the stuff comes to your home!".


No it's not. If the syntax is that similar, your metaphor makes no sense.


> I used to be a huge fan of async/await in the JS world, until I realized that we were tethering a very specific implementation to new syntax, without adding any clarity beyond what a coroutine and generator could do. eg `async function foo() { const a = await bar();` vs `co(function* foo() { const a = yield bar();` isn't really with adding extra syntax for.

FWIW Python originally used generators to implement async / coroutines, and then went back to add a dedicated syntax (PEP 492), because:

* the confusion of generators and async functions was unhelpful from both technical and documentation standpoints, it's great from a theoretical standpoint (that is async being sugar for generators can be OK, though care must be taken to not cross-pollute the protocols) but it sucks from a practical one

* async yield points make sense in places where generator yield points don't (or are inconvenient) e.g. asynchronous iterators (async for) or asynchronous context managers (async with). Hand-rolling it may not be realistic e.g.

    for a in b:
        a = await a
is not actually correct, because the async operation is likely the one which defines the end of iteration, so you'd actually have to replace a straightforward iteration with a complete desugaring of the for loop. Plus it means the initialisation of the iterator becomes very weird as you have an iterator yielding an iterator (the outer iterator representing the async operation and the inner representing the actual iterator):

    # this yield from drives the coroutine not the iteration
    it = yield from iter(b)
    while True:
        try:
            # see above
            a = yield from next(it)
        except StopIteration: # should handle both synchronous and asynchronous EOI signals, maybe?
            break
        else:
            # process element
versus

    async for a in b:
        # process element
TBF "blue functions" async remain way more convenient, but they're not always an easy option[0], or one without side-effects (depending on the language core, see gevent for python which does exactly that but does so by monkey patching built-in IO).

[0] assuming async ~ coroutines / userland threads / "green" threads


I do wonder if I'd feel differently if async + yield was a more common pattern. It can be extremely powerful (especially when combined with an event emitter). Maybe my problem is more with how async/await still isn't really being used for anything generators couldn't already do, but that's more about the current approach to using them than a limit of the language.


The reason we have await in JS is particularly because it is _less_ powerful than generators and you can make more assumptions about it and how one uses it as well as write better tooling.

This is why we have await in Python and C# as well, eventhough we know how to write async/await with coroutines for 10 years now :] Tooling and debugging is super important.


This is an important argument. Less powerful constructs are easier to combine and understand. For example, call/cc is very powerful, but I would prefer to use various less powerful constructs like try/except/finally which are clearer.


> `async function foo() { const a = await bar();` vs `co(function* foo() { const a = yield bar();` isn't really with adding extra syntax for.

I'd rather pick a standard solution over a library. I never used CO, because I didn't see the benefits of pulling this library in over plain ol' promises. The cognitive overload of generators, yielding, thunks and changing api-s is just too much IMHO, I like simpler things that work just as well.

With the await keyword baked into the language I can simply think of "if anything returns a promise, I can wait for the response in a simple assignment".


> I'd rather pick a standard solution over a library.

This. Rust can be complex enough for engineers new to the language. This sort of feature really requires a standard approach.


It's as standard a solution as any, it just doesn't require changing the language. If you can Promise.all, you can Promise.coroutine. Putting async in front of a function definition requires learning a new syntax, but wrapping a coroutine FN around it builds on knowledge of existing behaviour.


I spent quite a lot of time writing Typescript using Fluture and a few other libraries a while ago. Coming from a functional background, combinators + futures actually feels quite natural to me, so I looked at the examples of the "bad" code here and thought I actually quite liked it. But I certainly don't mind the async syntax too much either. Will see how it works out some time!


It's not just about the combinators vs specialized syntax. I've had to write a library with 0.1 futures and you have to implement a lot of Futures manually. i.e. you need to implement the Future trait for your types pretty often. In those cases you cant use the combinators, you need to directly drive the state machine of your future with poll() and try_ready macro etc. With async/await I don't have to implement any futures manually anymore, the diff of my library updating to async/await is basically deleting 75% of my code.


Yeah and this is huge. Not something I realised I needed coming from my experience in js, but it's a big deal in rust. It's not that different from needing to manually branch match arms and `into` your errors back for `Result`s, when those could all be communicated just as clearly with the far more concise `?`. The `.await` removes so much re-implementing the Future trait. It's been especially bothersome for me, because often I'm trying to figure out how this particular library implemented their Future. Now I feel like if I get the gist of how a Result is returned, I can apply identical reasoning to how `.await` is returning my Future, except that it's suspending the function and saving the state. Obviously that's what a generator does, but the `.await` syntax is far more expressive than `await` in js, because passing back that `Result` is a much more difficult thing to manage by hand.


All that redundant noise is one of the main reasons why I avoid Rust if I can write something in Haskell. Rust has it everywhere, what is expected for a low level language, but as long as they can remove it, they indeed should.


Coming from mostly Js/ts, I've had the opposite experience. I'm finding what I write on rust may be more explicit, but often less noisy. Using an error wrapping lib and ? everywhere made the biggest difference. May not be as efficient as the most optimized rust code, but far faster than the equivalent JS for the same thing. I also pray every day for macros to make there way into ts, since I'm constantly dealing with friction on the boundaries of runtime vs static code that rust handles nicely with a macro.


Oh the combinators in rust can be great. I think making the code read more imperatively is often a case of confusing the familiar with the simple. TS makes combinators way better too. That's why it's the `.await?` that sells me on the whole concept. Writing an error-path separately can be a headache, and as a result I still have daemons that have some dependency with an `unwrap` that sits there, poised to crash everything without warning. The `?` has cut the length of some of my modules in half. I've been waiting for a good way to do the same in async code for a while.


> `async function foo() { const a = await bar();` vs `co(function* foo() { const a = yield bar();` isn't really with adding extra syntax for.

Except in the latter you have to bring your own `co(...)` function. Baking it into the syntax means you don't need that as a library.


Not really - you have your own promise implementation as `Promise`. Bluebird made `Promise.coroutine`, and that could've gone into the main Promise implementation.


in my experience, "async" allows you to write the code linearly, but now the code is executed out of order, which is actually much more confusing (for me) to debug and deal with. however, I'm interested to try the rust version to see whether the more explicit types, etc. make this easier to deal with than e.g. `async def` in python, which I find hideously complicated compared to normal event loops.


Double as confusing if you're used to taking a more fp approach, too. I definitely went overboard with await when it first came to JS, but nowadays I've found a new respect for .then. Especially in TypeScript where it's easier to chain the functions and worry less you're using them wrong. Many of the functions I'll write for a .then will find their way into an Array#map or an observable too.


> in my experience, "async" allows you to write the code linearly, but now the code is executed out of order

Nope, the code is executed in order. That's what `await` accomplishes. Wherever you use `await` you have a guarantee that anything that comes after it will only execute when the promise that was awaited, has resolved.


As a fairly new rustacean myself, I can confirm that the pre-syntax examples are incomprehensible gibberish to me. The bits with `await` are much simpler to make sense of.


You'd be doing something similar with the task library in C# or Promises in Javascript (or in ES5-land, callback hell).

I'd recommend just opening up an IDE in any of these and seeing the types and parameters that come in and out of these calls. The sync vs async worlds really to clash with each other but at least we have strongly typed languages to help with writing the code. In javascript you could be mixing callbacks and promises and encounter bits where you'd never know that the next line would never be executed because of some nonexistent callback, etc etc.


You'll still need to write enum state machines if you want to implement custom Streams (since libstd is only adding Future at the moment).

Even with Futures there can be times when you need the fine-grained control over execution that you don't get with async-await. Eg if you want to implement a `struct TimeOut<F: Future> { inner: F, timeout: std::time::Instant }` from scratch, you can't do that with async-await.


Now being able to just dig into the lower level of Futures at work, I'd say even that is not that hard. You just write the `poll` function and then pattern match inside. There are even nice macros, such as the `ready!` that helps you with some of the boilerplate.

The trickiest thing for me to grasp first was the `Pin` type. Like how does this prevent the value to be moved... Until I realized it's `Pin<&mut T>` and then I had my a-ha moment.


It's slightly more complicated than just pattern-matching.

- In general you have to be careful that if you're returning Poll::Pending, it's after something has registered a wakeup, otherwise your future is going to just stop.

- As a specific case, your match has to be inside a loop so that a state transition results in the new state being polled immediately. You can't just return and expect the caller to poll() you since there will be no wakeup registered that triggers the caller to do so.

- Because futures receive `&mut Self` rather than `Self`, it's difficult to do a state transition where the new state needs to own some properties of the current state. Eg `State::Foo { id: String } -> State::Bar { id: String }` Instead you have to do Option::take / std::mem::replace shenanigans on the state fields, or have an ugly State::Invalid that you can std::mem::replace the whole state with.


A bit confusing at first, but quite the novelty to use something like rustdoc and docs.rs as a blogging platform.


It's become a bit of a tradition, with the new rustacean[0] being the first to do it I believe, as far back as 2015. rustdocs being just markdown with the convenient aspect of testing that your rustdoc actually works makes it a really good target for this kind of blogging!

[0] https://newrustacean.com/show_notes/


People in the Racket community also sometimes to use Scribble[1] to write their blogs.

https://docs.racket-lang.org/scribble/


I was confused as well. I thought it was the official Rust documentation until I read your comment.


Slightly OT, but in case anyone else is curious, official Rust documentation lives at doc.rust-lang.org. docs.rs is auto-generated docs for packages published on crates.io.


docs.rs is run by the Rust team these days, so it is also "official", but documents un-official things.


Fair enough! "documents un-official things" is what I intended to imply; apologies if I wasn't clear


It's chill! It used to be its own thing, and I'm not sure if people know that it's not or not anymore :)


Now that I think of it, I'm not actually sure whether I knew before that it was official or not. I've definitely been using it for a long time now though, so I'm happy to hear it's in good hands!


I'm building an async server application (https://github.com/iddan/minerva) with async Rust and I feel the pain of not having await. Every time I'm using a Future I need to care about borrowing and Error types that if it was sync code I won't.


Am I the only one who finds it incredibly hard to grok how async/await and "regular" sequential code interplay? In JavaScript I strongly prefer the regular Promise way of doing things because async feels like it's mixing metaphors. Being relatively new to Rust it's not totally clear to me whether the Futures example in the article could be made more readable - it's definitely pretty gross - but I have to wonder.


It took me a while to get comfortable with it too. The intuition I use is that in the past it used to be DrawWindow(), DrawWindow(), DisplayResultsofSlowFunction() and then the window would freeze for a few seconds, but with async/await theres a queue basically and your DisplayResultsOfSlowFunction() gets put to the side while awaiting so DrawWindow() can keep doin what its doin and your app doesnt appear to freeze, for a gui example (idk if that helps or if ye already knew it but just in case, for me all the internet explanations never talked about the scheduler for some reason so it took me a while).


Note that futures, promises, &c. are all about deliberately asynchronous operations, most commonly I/O. Expressed in older terms, it’s cooperative multitasking.

Thus, when you have a CPU-bound function, they are of no assistance in UI responsiveness, unless you deliberately insert yield points; in JavaScript, for example, you might do a unit of work, or process work until, say, ten milliseconds have elapsed, and then call setTimeout(keepGoing, 0). (Critically in JavaScript, you mustn’t use `Promise.resolve().then(keepGoing)`, because that’ll end up in the microtask queue which must be flushed empty before the browser will render, so you won’t actually yield to the UI with this approach.)

So be careful about your DisplayResultsOfSlowFunction(). If its slowness is that it takes a long time to calculate, and it doesn’t actually have any asynchronous stuff in it, async will not help you.


I understand how they work. It's just that turning something as fundamental as "each semicoloned statement happens after the previous one, as quickly as possible", into an abstraction, makes me really uncomfortable.


It's different in a statically-typed language. The compiler and editor tooling keeps you aware at all times of that interplay. If you forget, you're reminded rather quickly.


> Although the more ergonomic syntax is not ready yet, asynchronous I/O is already possible in Rust with ecosystem around futures, mio, and tokio.

Does this mean all 3 of those crates are required to achieve async I/O?

tokio is a large project. Which part of tokio specifically is supposed to be used in conjunction with futures + mio to achieve asynchronous I/O?

Edit: it seems futures + mio are dependencies of tokio.


Futures are the interface for asynchronous code. You need some sort of executor to actually drive the chain of futures to completion. Tokio is the most popular option, but is not required, strictly speaking. You can whip up a simple executor with nothing but the standard library. It won’t perform as well as Tokio, of course. There’s a good reason it’s a big project.

(Tokio is built on top of mio so if you’re using Tokio, you’re using mio)


Doesn't futures 0.3-alpha bring its own executor? I haven't followed Tokio too closely recently, but my understanding was that, going forward, futures provides the executor and Tokio provides the reactor. At least, that would be a division that would make sense to me.


futures-preview 0.3 does have futures::executor::ThreadPool; a simple thread-pool based executor. However, I thought that tokio did its own thing here. I could be wrong.


That is right. And while it theory having a separate executor (eg in the futures lib) and IO providers (timers, sockets) in another lib works it will in practice never be as efficient as having an integrated system like Tokio. The reason for that is that decoupled solutions can’t integrate the reactors in the same eventloop and there is always extra synchronization and thread hopping involved. The difference can be huge, eg 50% less throughout on a server.


As a pure C & C++ programmer, I was amazed of the (to my eyes) incredibly clever little trick that is the Duff's Device [0], and started using it right away to provide embedded programs with easy to write "concurrency" [1], [2].

Some time later I had to dip my toes into the world of Javascript, learning first about Promises, and finally reading about async/await. I just realized that it is basically the same trick I had been using all along. And now it's coming to Rust, neat!

[0]: https://web.archive.org/web/20190217162607/http://www.drdobb...

[1]: http://dunkels.com/adam/pt/

[2]: http://dunkels.com/adam/pt/expansion.html


I don't see how Duff's Device provides "concurrency"? It's a way to unrolls loops. Now surplanted by SIMD instructions.

Or do you mean state machines?



Actually I meant "coroutines".

You create a main infinite loop in your application, in which you call all functions that are meant to perform activities that should be happening "at the same time", like for example taking ADC measurements, while updating a PID control algorithm, while waiting for the GPRS chip to connect to the internet; those would be their own function, called "protothread" if you use the Adam Dunkel's library, and they are going to run a number of blocking operations, waiting for some external hardware to do their thing.

Each one of those functions is converted into a Duff device by means of the macros provided by the Protothreads library.

Now each protothread function can express its own operations in a linear and very clear way, and yield execution to the other functions by just returning from the Duff device. When the main loop starts over, each of those functions will "resume" their operation at the point where they yielded during the last iteration.

The way I see it, this trick is much more than just loop unrolling; it provides a C developer with the same basic features that an async function would have in JS; and the yield points in the functions would be equivalent to the 'await' calls. This allows for very easy to read code, being able to follow the logic of each individual "thread" without much of the noise needed for taking care of the "coroutines" part. The next step (in the embedded world, at least) would be going the route of an RTOS, such as FreeRTOS, but I've managed well so far without it.

EDIT: and yes, this is basically also a neat was of streamlining a state machine


I assume that what the OP meant by Duff's Device is "switch in C is syntactically a goto" rather than the specific loop-unrolling paradigm of Duff's Device itself. An example usage I've come across is the bzip2 decompressor: https://github.com/enthought/bzip2-1.0.6/blob/288acf97a15d55...

(note that the case statements are hidden in all of the GET_* macro calls).


Maybe I'm really dense, but what on earth does loop unrolling have to do with concurrency or asynchronous code?


Cooperative multitasking is typically done with a high level loop pumping a state machine. Duff's device is often quite useful in that implementation.


There's been a long history of refinements leading to this.

Fantastic work! Look forward to the async/await simplicity


Can somebody explain the performance characteristics? For instance, let’s say I have a deep stack of 15 async calls. If the deepest call yields, what code is executed? Does it “jump back” to the top ala “longjmp” or it needs to return back up the stack frame by frame? And what happens when it resumes execution? IOW is the overhead linear with the stack depth or not?


You are correct, the overhead is linear with the stack depth. I suggested a more longjmp-esque approach [here](https://internals.rust-lang.org/t/pre-rfc-cps-transform-for-...) early on, but it didn't seem to be worth the extra implementation effort. Benchmarking never showed this linear overhead to be an issue.

The current thinking from the team seems to be that, if this does become an issue, a) inlining before state machine transformation can flatten this out quite a bit and b) convincing LLVM to do what would be required is a lot of work.


Also, you as the user always have the option of manually reducing the depth of the stack - spawn() a separate future with a one-shot channel sender and .await its receiver.


>If the deepest call yields, what code is executed? Does it “jump back” to the top ala “longjmp” or it needs to return back up the stack frame by frame?

It goes back frame-by-frame. Yielding (ie returning `Poll::Pending`) is essentially returning "I'm not ready yet" and it's possible the parent future wants to do something else in that case.

The simplest case is a future that wraps a collection of futures and resolves to the value of the first one that resolves. In this case it would cycle through each child future and only return `Pending` if all of them return `Pending`.

Another case is that of a timeout future that wraps another future - it would resolve to `Err(TimedOut)` if the wrapped future returns `Pending` for too long.

>And what happens when it resumes execution?

The top-most future of the task is all the executor knows about, so the executor has no choice but to start from there.


There have been comments in this HN thread that this implementation of continuations based on futures is supposedly more performant than a stack-pointer switching one (like green threads). Can you elaborate on how? It looks to me that it should instead be slower (and even much slower if the stack depth is deep).


It cannot be more "performant". The design of demand-driven futures is used because it allows "zero-cost futures". Specifically poll()ing the Future is required to drive it, and drop()ping the Future frees its resources. Both of these require another future or the executor itself to be the parent of every future, rather than the future driving itself.

But also the performance is not likely something to worry about. I've never measured it myself but I do know aturon posted that he could not find any measurable effect of having deep future stacks. It is noticeable in the sense that a big enough stack may exceed your OS's limit (especially on Windows where the main thread defaults to a 1MiB stack).


Yet another 1000-word rederivation of a monad, yawn. Wake me up when you've realised you might as well just write Haskell.


Hey, could you please not post in the flamewar style here? We're trying for something other than that on HN, including avoiding old-school programming language flamewars. It's been many years since any of those were fresh.

https://news.ycombinator.com/newsguidelines.html


You're missing the forest for the trees. While in some abstract sense, async/await is monadic, sure, Rust does not have the ability to express the monad abstraction, and it's unclear if it ever will be able to. Those details are what makes this topic interesting.


from my previous interactions with the rust years ago and related rust design discussions on their issue tracker,the main barrier seemed to be rooted in the unfortunate "self" trick for how "the type im instantiating a trait with" syntax making higher kinded trait support expressible as a simple extension,

This is of course ignoring the related need to also write code parametrized over lifetime variable structure ..




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

Search: