[HN Gopher] In Defense of Async: Function Colors Are Rusty
       ___________________________________________________________________
        
       In Defense of Async: Function Colors Are Rusty
        
       Author : todsacerdoti
       Score  : 31 points
       Date   : 2022-01-04 10:45 UTC (1 days ago)
        
 (HTM) web link (www.thecodedmessage.com)
 (TXT) w3m dump (www.thecodedmessage.com)
        
       | jerf wrote:
       | I wouldn't call the coloring "a good thing". I would call it "an
       | acceptable compromise". It would be _better_ if you didn 't have
       | to label it and it all Just Worked, just like it would be
       | _better_ if you didn 't have to annotate lifetimes and it all
       | Just Worked, but for the space Rust is operating in, it is an
       | acceptable compromise. Rust sits high on the "force programmer to
       | put more effort in, give programmer more out in return", on a
       | pretty nice cost/benefit tradeoff place in that space. But the
       | fact you get benefits commensurate with the costs doesn't mean
       | it's not a cost at all, and it's probably not good to get into
       | the habit of conflating costs and benefits. Clarity of view of
       | both sides of the balance is very important for engineers.
        
         | jayd16 wrote:
         | What would be "just working" in your mind WRT promise, unwrap
         | and thread context semantics?
         | 
         | Sugar around thread yield, unwrap and context return? That
         | seems like a lot of possible gotchas to simply be hidden and
         | lurking under every benign looking synchronous method.
         | 
         | Remember that the point of async/await is cooperative threading
         | and not pre-emption. Hiding these details makes it much harder
         | to reason about. If you don't know when your execution will be
         | yielded, you're essentially being pre-empted, no?
        
       | yccs27 wrote:
       | Here's my (oversimplified) understanding of async: It's functions
       | with yield points, at which other code can be run until execution
       | returns to the function. Then, isn't it almost trivial to call an
       | async function from a synchronous context? Just skip over the
       | yield points?
        
         | jayd16 wrote:
         | It depends on the context of the yield but assuming the caller
         | and callee contexts can be the same, most async/await
         | implementations will not actually yield until some kind of
         | thread sleep or explicit yield is hit. So yes, this happens in
         | most languages I'm aware of.
        
       | munificent wrote:
       | I don't agree with the article that the idea of colored functions
       | is tied to dynamic types. I think it applies equally well to
       | Dart, TypeScript, C#, and any other language considering async
       | await. It's mostly a question of usability and the ability to
       | encapsulate use of asynchronous operations.
       | 
       | But for Rust, I think having colored functions is probably the
       | right call. Asynchronous operations _are_ fundamentally different
       | at the OS /machine level. Since Rust is a language where code
       | deliberately has very high affinity to what the underlying system
       | is doing, it makes sense to make that distinction visible to
       | users even if it means that it's harder to write and reuse
       | asynchronous code.
        
       | bruce343434 wrote:
       | What I don't understand is why these functions must be declared
       | async at the function declaration site. Why not allow the
       | programmer to run any expression asynchronously, which wraps the
       | return value of the expression in a future or promise or what
       | have you?                   let promise = async
       | {something+someother();};         // other computations
       | let result = await promise;
       | 
       | You'd essentially be creating a very ergonomical shortcut to
       | fork-join multithreading which is infinitely more powerful and
       | expressive than whatever this "async function" stuff is.
       | 
       | ETA: perhaps it would be a good idea to have a thread-unsafe
       | keyword, though I'm not sure if rust needs that since rust has a
       | really stringent resource pointer/reference aliasing guarantee.
       | 
       | ETA2: I misunderstood async semantics for rust, it's not a
       | fork/join but send/poll. In any case, I still think you should be
       | able to declare async at the call site, not the function site.
       | That doesn't seem to clash.
        
         | [deleted]
        
         | throw10920 wrote:
         | > I still think you should be able to declare async at the call
         | site, not the function site
         | 
         | This is the big point. I'd love to see anyone put forth a
         | concrete, valid reason why this couldn't be implemented,
         | because as it stands, it appears that the function coloring
         | problem is _exclusively_ due to poor language design, rather
         | than a technical limitation (where an example of a technical
         | limitation would be escape analysis, which is hard).
        
           | jayd16 wrote:
           | Explicit await and promise semantics are useful incase you
           | want to, say, start multiple async calls instead of awaiting
           | one and then the other. But ok, lets say you just want await
           | to be implicit in some cases...
           | 
           | Do you actually want this? Currently, you can assume that a
           | function will not change threads mid execution, and that that
           | thread will also not be used to execute other things.
           | Implicit yielding breaks that assumption. Cooperative
           | multithreading becomes impossible because it's much hard to
           | reason about when a method call will yield execution or not.
        
           | henrydark wrote:
           | I think the CPS attempt in Nim could do this
           | 
           | https://github.com/nim-works/cps
        
         | jayd16 wrote:
         | Wrapping sync in async is usually not so hard but usually an
         | anti-pattern. In C# for example, if that synchronous work is
         | long running, it might consume a thread in the shared thread
         | pool and have side effects (starvation) elsewhere in the
         | application.
        
           | bruce343434 wrote:
           | I don't understand what you mean. What is to say that an
           | async-fn is not long running? It can just as well consume a
           | thread, right?
        
             | jayd16 wrote:
             | Well I can only speak to C# really but...
             | 
             | The preferred pattern is to avoid long running tasks in the
             | default thread pool as it's a shared resource. Long running
             | tasks should be executed somewhere else, either some new
             | thread pool or just a new thread or just don't use async
             | and make it your caller's problem. The point is that you
             | don't want to consume the limited shared resource.
             | 
             | In your case, what would happen? Would a new thread be
             | created? Is it ok to use the default pool? Did the
             | synchronous method writer even consider this? It's hard to
             | say.
             | 
             | Now, does this need to be enforced by the language? No.
             | Developers could simply append "_Async" to make it clear a
             | method can be put on the default thread pool or not. That's
             | still a form a coloring though. Even if you consider it a
             | choice by the caller, it's still a color that needs to be
             | determined and you never want to exhaust your default
             | thread pool.
        
             | lijogdfljk wrote:
             | I may not follow you - but to expand what _(i believe)_ the
             | GP comment said; any long running code - say a loop that
             | spins for an hour, is bad in the async context, because
             | that async thread is part of the runtime. Better to take
             | those special cases and let the executor move the
             | computation to a dedicated thread.
             | 
             | At my work i mix a lot of compute heavy work in an async
             | context, and if i let this compute heavy stuff run on my
             | async threads i'd potentially starve my runtime. Tasks
             | wouldn't get woken up on time, etc.
        
               | bruce343434 wrote:
               | right, but this isn't an argument against allowing
               | arbitrary async expressions. I can make an async-fn that
               | runs an infinite loop just as easy.
        
         | wizzwizz4 wrote:
         | > _You 'd essentially be creating a very ergonomical shortcut
         | to fork-join multithreading which is infinitely more powerful
         | and expressive than whatever this "async function" stuff is._
         | 
         | But that would require starting other threads, which requires
         | everything to be `Send + Sync`, has the overhead of
         | multithreading, _and_ can 't be managed with any sort of
         | scheduler (without platform-specific hacks). Unless you use
         | green threads, and that's discussed in the article.
        
           | bruce343434 wrote:
           | So what then is async? If there is no scheduling/preemption
           | in executing an "async" function, it's not really "async",
           | but rather synchronous. Is async just a new way to say
           | "instruction reordering"? Or is async really just "*defer*
           | until the last possible moment: await"?
           | 
           | edit to match yours: not sure what a "green" thread is.
           | 
           | In any case, if threads are not an option, an async may
           | always be ignorable, after all, a program may not rely on the
           | order of the execution of its threads/its preemption; that
           | would be a race condition. Textbook definition.
           | 
           | As I see it, the fork/join model is very useful for
           | "shingling" code paths that don't have to happen in sequence,
           | but _could_ happen in parallel.
           | 
           | Final edit: nothing is otherwise stopping the implementer of
           | async to add a pre-started threadpool to the runtime, with
           | whatever configurable parameters one would want.
           | 
           | Post final edit: I think fork/join vs send/sync are just
           | implementation details. But I respect Rust for going with
           | send/sync, just that it doesn't change all that much in terms
           | of the programming interface.
        
             | AndrewDucker wrote:
             | Async is running many tasks on many less threads, using
             | polling.
             | 
             | See https://rust-lang.github.io/async-
             | book/01_getting_started/02...
        
               | steveklabnik wrote:
               | (but note that this doesn't mean busy-loop polling, while
               | that will be used in "my first executor" tutorials,
               | production executors don't call a future until it's ready
               | to have more progress made via an event, often from an
               | epoll/kqueue/iocp or similar mechanism.)
        
               | bruce343434 wrote:
               | this sounds like a lot of overhead, though
        
               | Groxx wrote:
               | It is, relative to not doing it. But it's quite a lot
               | less overhead than a native thread.
        
               | chmod775 wrote:
               | As opposed to what?
        
               | DenseComet wrote:
               | Rather, Rust just makes this overhead explicit. Go, Node,
               | etc do the same thing, but just hide what actually
               | happens.
        
             | monocasa wrote:
             | It's "compile this as a state machine such that when other
             | futures are waited on internally by this function, store
             | stack state in an anonymous struct that implements Future +
             | Poll, and allow repumping when that Poll implemention says
             | it's ready. This allows you to reuse the same stack when
             | this async function is blocked"
        
       | jeff-davis wrote:
       | My complaint about the async rust world isn't so much function
       | coloring, but dependency craziness.
       | 
       | For instance, the postgres driver for rust (which is an excellent
       | driver, by the way), is built in on async primitives. That's a
       | good thing, because it means that it can be used effectively from
       | async code.
       | 
       | Unfortunately, a "hello world" program that uses the client takes
       | 38s to build and downloads 65 dependencies, many of which are at
       | 0.x.y versions and I have no idea what they do (parking_lot_core
       | v0.8.5? slab v0.4.5?).
       | 
       | This is not just an issue of build time, but overall confidence
       | in the quality and security of the code, and knowledge of who the
       | authors even are. Also, it just adds complexity and magic, and
       | sometimes I just want my code to be simple and tight and
       | understandable from beginning to end.
       | 
       | One idea to resolve this is if rust has a standard futures
       | runtime API, and you can just choose one in Cargo.toml or
       | something. By default it would be a built-in single-threaded
       | runtime, but you could choose tokio if you want. If you want to
       | call async code from sync code, there would be an easy way to do
       | it (maybe more syntax?) that would use whatever runtime you chose
       | in Cargo.toml.
        
         | jkarneges wrote:
         | I typically vet dependencies by doing reverse lookups, e.g.:
         | 
         | https://crates.io/crates/slab/reverse_dependencies
         | 
         | And then looking into the biggest results and understanding
         | community sentiment. Ultimately you need to trust the authors.
         | Do this enough and you start seeing the same popular projects,
         | making it easier to trust things.
         | 
         | > sometimes I just want my code to be simple and tight and
         | understandable from beginning to end
         | 
         | I hear ya! This is one reason I wrote my own async runtime, and
         | the output of "cargo build" fits on my screen. :) But I don't
         | recommend this unless you have a LOT of time.
        
           | michael_j_ward wrote:
           | It would be nice to subscribe to any change in that
           | dependency relationship. If `proxy-auditor-crate` upgrades,
           | then probably I should to.
           | 
           | And probably more importantly, if `proxy-auditor-crate`
           | _drops_ a dependency because they no longer trust it, then I
           | definitely want to be notified.
        
           | lijogdfljk wrote:
           | Yea, i don't really get the distrust of the ecosystem tbh.
           | Yea, i agree the `0.x` stuff is annoying/concerning, but the
           | volume of dependencies feels very.. meh. I use NixOS for
           | example. The number of things i download is _insane_ when i
           | build my system. This isn't a Rust or JavaScript "problem",
           | it's just how natural distribution of work occurs. People
           | build off of the work of their peers. And if you reduce the
           | friction of work distribution, like how NPM and Crates do,
           | you naturally get a lot - sometimes to a comical degree.
           | 
           | However are we going to expect crate authors to repeatedly
           | reinvent wheels? Would we trust them if they did?
        
         | davidkunz wrote:
         | JavaScript had the advantage that is was async first (it
         | started with callback-style APIs, but they can be converted to
         | Promises).
         | 
         | In Rust, this is not the case and leads to fragmentation.
         | 
         | There are a lot of high-quality libraries which cannot be
         | efficiently used in an async context (e.g. Diesel). And since
         | Rust isn't shipped with a default runtime (tokio, async-std,
         | ...), some crates are incompatible with the rest of your
         | program. I understand the reasoning (there is no "one size fits
         | all") but it makes things more complicated.
        
         | __s wrote:
         | Rust does have a standard futures runtime API _(edit: thinking
         | over, maybe you meant there should be a runtime in std)_
         | 
         | This then opens up alternative runtimes to tokio (async-std,
         | smol, glommio, monoio)
         | 
         | Then your suggestion of a single threaded executor exists
         | already: https://github.com/enlightware/simple-async-local-
         | executor
         | 
         | Granted, in practice many libraries end up building a hard
         | dependency on tokio
        
       | wizzwizz4 wrote:
       | > _In both these cases, it's like the meaning of having one
       | statement come after another has changed: ; itself has been
       | overriden._
       | 
       | I don't agree with this analogy. With Result, it's ?; - with
       | async, it's .await; . That's not overriding or overloading; it's
       | explicit.
        
       | Ericson2314 wrote:
       | The key part with the "monad" bit at the end is we have monad
       | polymoirphism allready, e.g..
       | 
       | foo :: Monad m => m ()
       | 
       | in Haskell. If that goes to rust, maybe we can have "async
       | polymorphism". This, and the far more easier "mut polymorphism"
       | (no more as as_mut boilerplate) means we get best of both worlds:
       | color and code reuse!
        
       ___________________________________________________________________
       (page generated 2022-01-05 23:00 UTC)