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

A bit off-topic: Is there any theoretical reason you need async / await syntax at all?

(It's certainly desirable for performance and compatibility to avoid making all subroutines into coroutines, so I understand why most languages have done this.)

And restricting it to the case where coroutines have a single return...

Subroutines are naturally coroutines that don't yield. And it seems like the question of whether it's a subroutine or a coroutine shouldn't be something the programmer needs to worry about.

What I've seen in coroutine libraries is the main reason we need await seems to be for cases when we don't want to await a result.

If you need an unawaited invocation to pass to a routine like gather, you do this:

    my_results = await gather(run_job(x) for x in work)
But, even if we can't infer from the type of gather that it must accept a future, a function can have a method that asks it to return a future:

    my_results = gather(run_job.future(x) for x in work)
You'd often want to dispense with gather. If I have these statements:

    alpha = run_alpha()
    beta = run_beta()
    gamma = run_gamma()
    return alpha + beta + gamma
In most cases, e.g. if these were requests going over the network, we'd prefer to schedule all three at once. If we want to sequence side-effects, then explicit await makes sense:

    alpha = run_alpha.await()
    beta = run_beta.await()
    gamma = run_gamma.await()
    return alpha + beta + gamma
That makes the most concurrent option easiest, as opposed to the clunky idiom of "gathering" many results.


> Is there any theoretical reason you need async / await syntax at all? [...] That makes the most concurrent option easiest, as opposed to the clunky idiom of "gathering" many results.

Futures / async-await is an idiom that makes async code easier to reason about, not something that provides any new theoretical foundation or any functionality that wasn't possible before. Like a type system, it's strictly for safety & convenience.

Whether & where you wait on any individual async request is largely separate from the await syntax. If you can start 3 async requests in parallel, and await on the group rather than each request, you probably should.

That said, the idiom of gathering results sequentially is guaranteed to work and be safe, where hidden dependencies and hard to identify race conditions have plagued attempts to make parallel async code since the beginning of time. The syntax of futures in various languages is making async and parallel code easier and safer for most programmers to use without landing in quicksand.


> Futures / async-await is an idiom that makes async code easier to reason about, not something that provides any new theoretical foundation or any functionality that wasn't possible before.

So, this is true in GC'd languages, but if you see my link below, in Rust, async/await does let you write code that was previously impossible. This is because the compiler can't understand lifetimes in the way that the code is written with raw futures.

In theory, someday, if and when generators are stabilized, this will be true again, but instead with the caveat of "async/await doesn't give you any new thing that wasn't possible before, except that it can do it with safe code, rather than unsafe code."


I meant my comment a bit more broadly & abstractly than that. You can write code in a new way with await, and it does help you with how to factor & manage the code. But it does not fundamentally let you make parallel requests that you couldn't make before, somehow. The code you needed before await might be ugly and stateful and not idiomatic, but it was still possible to make requests and wait for them, before await arrived.

What we have now with await does change the game, it makes async code appear synchronous, and the advantages of that change should not be underestimated. I think futures & await are huge and positive changes for every language they're showing up in, and they do allow for new kinds of programming, I'm just answering the parent comment's question about whether they're strictly necessary in theory. The answer is that they're not.


Okay, yeah, that is fair. I have written enough raw futures in anger to be like "it's not practically possible", but you're also right that it is actually practically possible, regardless.


Oh, practical is also separate from my answer, since my reading of the question was strictly "theoretical". ;)

Maybe about a decade ago I wrote a raw futures system for async asset loading off DVDs on the Nintendo Wii in C++, and it had to play nice with the real-time rendering system. I didn't stand back far enough to abstract it the way futures work today, and as a result it was an absolutely awful experience. It was so difficult to understand and debug, I dug myself an enormous hole. Having used futures in JavaScript and Scala and other languages since, I'm certain having real futures would have saved me months of crunching.

I would agree that await syntax brings a huge degree of practicality to what was previously theoretically possible.


In an abstract sense, no. In rust, it matters a lot. The first reason is that we don’t have a runtime. The second is stuff like http://aturon.github.io/2018/04/24/async-borrowing/


Thanks for the link, I'm definitely interested in how it's done without a runtime. I know in theory a coroutine can compile down to a gory switch statement and some state, but doing anything useful with it gets hairy, I'm sure.


So, you do need to include some code to drive the futures. The key is that this is a library in Rust, rather than built into the language, and so Rust programs that don't use async do not pay the cost.

And yeah, that's sort of how it works under the covers :)


I think (but I cannot find a reference to now) one of the proposed syntaxes was that awaits were immediate and implicit in async functions, and that if you didn't want that, you could opt-out and get futures normally. Loosely,

  async fn foo() {
    let result = blocking_req();  // awaiting immediately here.
    async {
      let future = blocking_req();  // not awaited.
      // can do more complex stuff w/ the future
    }
  }
IDK what happened to it, and it is a bit more complex IMO than what the Rust folks went with. However, I don't think it helps your gather case.

For gather, I think the problem w/ your proposed

  return alpha + beta + gamma
is when does that happen? If I do

  let some = alpha + beta;
  // < ... code ... >
  let more = some + gamma;
what happens / when are things evaluated?

At any rate, with the syntax as it is, I think your gather example is essentially¹:

  alpha.join3(beta, gamma).await
or

  Future::join3(alpha, beta, gamma).await
(Perhaps minus the `.await`, depending on when you need/want the result.)

There's no operator overloading, which I'm fine w/, as there is a need to choose between, e.g., select or join.

¹but I'm new w/ the async stuff, so if I'm wrong I'm assuming that Cunningham's Law will kick in and someone will correct me.


Here's the proposal: https://internals.rust-lang.org/t/explicit-future-constructi...

One of the goals behind it was to align async control flow with sync/thread-based control flow, to make it more intuitively obvious when things would run concurrently and when they would not. That is, calling a function, even if async, would immediately run it to completion, while wrapping it up in a closure or async block that gets spawned would run it concurrently.

This is especially relevant to Rust, which departs from other implementations of async/await in that merely constructing a future is insufficient to start it running, which is why you need that `join3` call to explicitly introduce concurrency.

Under the "implicit await" proposal, your first example would look like this:

    async fn foo() {
        let result = blocking_req(); // await immediately
        let future = async { blocking_req() }; // think of async{} like a lambda function
        // can do more complex stuff with the future, like:
        //  future.await()
        //  Future::join(future, other_future)
        //  tokio::spawn(future)
    }
The thread-based equivalent looks very similar:

    fn foo() {
        let result = blocking_req(); // block the current thread immediately
        let closure = || { blocking_req() }; // don't run this yet
        // can do more complex stuff with the closure, like:
        //  closure()
        //  rayon::join(closure, other_closure)
        //  thread::spawn(closure)
    }


The async block syntax would be nice, and probably far more explicit.

I think with the `return alpha + beta + gamma` the idea is that within a scope, everything is done as late as possible, but you can't leave a scope without all results.

Thus, if the code is expanded as:

    alpha = run_a()
    beta = run_b()
    gamma = run_c()
    a_plus_b = plus(alpha, beta)
    sum = plus(a_plus_b, gamma)
    return sum
In that case, the compiler has to determine a partial ordering:

    a_plus_b <- run_a, run_b
    sum <- a_plus_b, run_c
Thus it can generate:

    await gather(run_a, run_b, run_c)
    await plus(alpha, beta)
    await plus(a_plus_b, gamma)
And presumably you can detect pure subroutine / primitive invocations.

And there would be other hard points where tasks must be fully awaited; e.g. loops wouldn't be able to leak tasks.


Indeed, I think going forward we will see high-level languages that paper over the distinction between synchronous and asynchronous functions. However for Rust specifically, the performance implications you note are of particular concern, while the remark "it seems like the question of whether it's a subroutine or a coroutine shouldn't be something the programmer needs to worry about" is debatable when considering languages that aim to sit at the same level of the stack as C.


I should revise that: "programmers aren't very good at deciding whether a function should be a subroutine or coroutine."

Specifically, the problem is that making the decision happen requires refactoring and that can make a small change into a big one. My contention is that for some languages (probably not Rust) it would be beneficial if this didn't impact the code structure heavily.

I've seen smart people simply avoid trying to use asynchronous code in Javascript because it "poisons" the whole stack. Part of this was because it was based on promises, and it meant going through a bunch of logic and refactoring. Arguing against my case, that did result in a slightly nicer structure overall.

Hopefully having proper async / await notation avoids 99% of that mess in Rust by making it a matter of decorating code with the appropriate keywords and being a bit more thoughtful about structure.

And you're obviously right that Rust needs to have sane performance characteristics, let alone being able to export C compatible bindings.




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

Search: