[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)