[HN Gopher] Let futures be futures
       ___________________________________________________________________
        
       Let futures be futures
        
       Author : ingve
       Score  : 51 points
       Date   : 2024-02-03 18:02 UTC (4 hours ago)
        
 (HTM) web link (without.boats)
 (TXT) w3m dump (without.boats)
        
       | quietbritishjim wrote:
       | That is a depressingly large amount of text to write about
       | futures without referencing or even seemingly being aware of
       | _Notes on structured concurrency, or: Go statement considered
       | harmful_ [1] or the Python library Trio that resulted from that.
       | Trio doesn 't have any future or task objects _at all_. To be
       | honest, it is occasionally annoying (mainly for fetching the
       | return value of a background task) but far far less often than
       | you 'd expect. For composing operations, you are generally much
       | better off using using Trio nurseries (or task groups as asyncio
       | calls them - having copied the idea from Trio!) and channels for
       | communication between them if needed.
       | 
       | [1] https://vorpus.org/blog/notes-on-structured-concurrency-
       | or-g...
        
         | TwentyPosts wrote:
         | Thank you, that article looks quite interesting and I look
         | forward to giving it a proper read.
        
         | withoutboats3 wrote:
         | Sorry my blog post depressed you, but I wonder if it did so
         | nearly as much as your arrogant & rude comment did me.
         | 
         | I am deeply aware of njs's notes on structured concurrency &
         | have been working on a post engaging with it. This post was not
         | about structured concurrency & so it wasn't relevant here.
        
           | airstrike wrote:
           | Many of us loved the article but didn't have anything
           | worthwhile to comment, so maybe one consolation is that the
           | one rude comment is just the one that gets posted but not
           | representative of people's overall reception to the article.
           | You can't please everyone, as they say
        
       | mjevans wrote:
       | Pseudo-golang with generics
       | 
       | func yield[G AnyGenericEvenInterface](promise G) G {...}
       | 
       | I'd have preferred the language expand yield() to take an
       | argument which yields until the ((promise)d) variable is resolved
       | to a value. This makes it more clear that the work is being
       | passed off to runtime and will wait for that value.
       | 
       | I'm still somewhat confused that promise constructions don't have
       | any well defined start time https://developer.mozilla.org/en-
       | US/docs/Web/JavaScript/Refe... They could start instantly or
       | defer entirely until waited on by any of the resolution methods.
       | It seems more complex than light threads / goroutines
       | synchronized by communications queues and other first class
       | language objects.
        
       | TwentyPosts wrote:
       | When I read this, I have to think of a take I heard a few times,
       | namely that "Rust would be better off if the language had opted
       | for a proper effect system from the get go, instead of grafting
       | specialized forms of it onto the language now."
       | 
       | Does anyone have some thoughts on this? I just lack the knowledge
       | to really evaluate that sort of take. There's a lot of
       | discussions on whether async Rust is "good" or "bad", or the
       | right abstraction or not, but it's pretty clear that (at this
       | point) we're more less stuck with it (for better or worse), but
       | that's not what I am asking about.
       | 
       | What I am asking is whether a proper effect system would have
       | genuinely, and legitimately improved or significantly simplified
       | the whole state of Rust async or not, and how. A lot of the async
       | Rust problems sound like "hard" problems, and I'm a bit skeptical
       | that a feature like that would really reduce the "heavy lifting"
       | you have to do.
        
         | withoutboats3 wrote:
         | This is the subject of the final section of this post, effect
         | systems specifically are referenced in the margin note.
        
         | zozbot234 wrote:
         | > Does anyone have some thoughts on this?
         | 
         | Discussed most recently @
         | https://www.abubalay.com/blog/2024/01/14/rust-effect-lowerin...
         | / https://news.ycombinator.com/item?id=39005780 It's a viable
         | approach if you can take the time to get it right. Async rust
         | started out with more of a limited, MVP approach, where they
         | first did the simplest thing that would surely work, and then
         | went on from there.
        
       | Animats wrote:
       | I've had to build something like an explicit futures system for a
       | common case that doesn't look like an async pseudo-thread. There
       | are a large number of requests for various assets. When an asset
       | arrives from the network, all the objects waiting for it need to
       | be informed so they can update. Individual objects may have
       | multiple updates pending. Updates need not arrive in order. It's
       | a many to many relationship, not a 1-1 relationship.
       | 
       | This is not uncommon in big-world games. I hit it in my metaverse
       | client. It's a standard Windows feature. Windows file systems
       | have "FindFirstChangeNotificationA", which lets a process monitor
       | a collection of files for modifications. That has scaling issues
       | for large numbers of files, but it's the same problem. Similar
       | ideas exist in the database world. You'd like to be able to ask
       | "tell me when the results of this SELECT change". That's been
       | played with as a concept, but didn't go mainstream.
       | 
       | In this area you can turn a manageable special case into a really
       | hard generic problem. This is useful if you need a thesis topic,
       | less useful if you need a working system.
        
         | golergka wrote:
         | I've worked with similar systems in gamedev. To be exact, we
         | downloaded 2d images and then managed image atlases with them
         | -- so we needed to update renderers not only when image they
         | required was downloaded and included in an atlas, but also when
         | an atlas with their image was rebuilt.
         | 
         | To be honest, the system that we designed wasn't that
         | dissimilar from promises. It was a dynamic collection of
         | callbacks (using built-in C# events), not unlike then/catch
         | delegates. I've worked on it about 10-12 years ago, exactly
         | when JS world had discussions on what flavour of promises was
         | the best, and it was one of the main sources of inspiration.
        
       | jcranmer wrote:
       | What I like about this post is that it provides a good attack
       | against the function coloring philosophy, which always rubbed me
       | the wrong way. Function coloring essentially implies that there's
       | no good reason for async and sync to be different, and it's being
       | used as an argument that we should try to sweep the distinction
       | between the two under the rug as much as possible, and that's
       | just not a good idea.
       | 
       | I also like the point that maybe(async) isn't terribly
       | compelling. Recently, I've been looking at building an async
       | parser, and the sync version of the interface is pretty simple:
       | fn parse(r: &mut dyn Read) -> Result<ParsedObj> [1]. In theory, I
       | could slap an async on it, change Read to AsyncRead, and now I
       | have an async version. Except it's not usefully async: the
       | consumer of the data can't do anything until everything has been
       | fully read in. So what you want in the asynchronous version of
       | the API is something that looks like a stream of parse events.
       | But that API is way too much faff for anyone trying to use the
       | API synchronously. Or is it? If you have a high-level method that
       | distilled the stream of output events into a simple, easy-to-use
       | answer, then you end up with something that looks
       | maybe(async)-able [2].
       | 
       | Where I think the author gets it wrong, though, is in asserting
       | that it's statefulness that causes maybe(async) to break down.
       | State per se isn't the problem: our underlying parser above is
       | statefully turning a stream of input data into a stream of parse
       | events, and this could sometimes work well with maybe(async). The
       | problem arises when you're multiplexing many streams at once. In
       | synchronous code, there's just no way to do that kind of
       | multiplexing (as pointed out earlier in the blog post), while
       | it's a pretty key design feature of async code. The trouble
       | really comes in when you have a system whose outer interface is a
       | 1:1 stream interface, but which internally needs to go through a
       | 1:N piece and an N:1 piece. It looks like it could be
       | maybe(async)-able from the outside, but its implementation is
       | hopelessly not maybe(async)-able.
       | 
       | [1] Honestly, a lot of parsers go a step further and just read in
       | the input buffer as a &[u8] and rely on zero-copy, but that
       | interface turns out to be even less useful in an async context.
       | 
       | [2] But the faff remains if you can't resort to one of the pre-
       | canned methods. How much of an issue that ends up being is left
       | to the reader to decide.
        
         | withoutboats3 wrote:
         | You're right, state isn't really what I meant. I meant exactly
         | the sort of concurrent collating you allude to.
        
       | airstrike wrote:
       | _> If you were a language designer of some renown, you might
       | convince a large and wealthy technology company to fund your work
       | on a new language which isn't so beholden to C runtime,
       | especially if you had a sterling reputation as a systems engineer
       | with a deep knowledge of C and UNIX and could leverage that (and
       | the reputation of the company) to get rapid adoption of your
       | language. Having achieved such an influential position, you might
       | introduce a new paradigm, like stackful coroutines or effect
       | handlers, liberating programmers from the false choice between
       | threads and futures. If Liebniz is right that we live in the best
       | of all possible worlds, surely this is what you would do with
       | that once in a generation opportunity._
       | 
       | This seems... a bit specific? Was it a reference to a specific
       | new language or was that more of a poignant wish?
        
         | opnitro wrote:
         | I'm guessing it's a reference to Go
        
         | zozbot234 wrote:
         | > This seems... a bit specific? Was it a reference to a
         | specific new language or was that more of a poignant wish?
         | 
         | Not very new, seeing as Erlang was open sourced by Ericsson in
         | 1998. (The language itself is from 1986 but was proprietary up
         | to that point.)
        
         | wging wrote:
         | It's Go. The description fits well, and it's pretty much
         | confirmed by the Rob Pike quote in the sidebar.
         | https://www.youtube.com/watch?v=uwajp0g-bY4
        
         | Georgelemental wrote:
         | From the following paragraph:
         | 
         | > You might _go_ do that, in a less than optimal world.
        
       | amluto wrote:
       | > Rust chose this approach to get zero-cost FFI to the enormous
       | amounts of existing C and C++ code written using that model, and
       | because the C runtime is the shared minimum of all mainstream
       | platforms. But this runtime model is incompatible with stackful
       | coroutines, so Rust needed to introduce a stackless coroutine
       | mechanism instead.
       | 
       | Can anyone (withoutboats?) elaborate on this? If "stackful"
       | coroutines means functions with an ordinary-ish stack that can
       | yield, then those stacks are just memory, and you can take
       | references to them. Heck, you can do this, slowly and painfully,
       | in C, with setcontext. And you can mostly do FFI with a degree of
       | care about stack sizes. You can even yield across FFI boundaries.
       | In some sense, the coroutine objects implicitly created by Rust
       | and C++ are really just stacks minus the hardware stack pointer
       | part.
       | 
       | Also, I found this amusing as a kernel programmer:
       | 
       | > the implementations of each of these things are completely
       | different. The code that runs when you spawn an async task is
       | nothing like spawning a thread, and the definition and
       | implementation (for example) of an async lock is very different
       | from a blocking lock: usually they will use an atomics-based lock
       | under the hood with the addition of a queue of tasks that are
       | waiting for the lock. Instead of blocking the thread, they put
       | this task into that queue and yield; when the lock is freed, they
       | wake the first task in the queue to allow it to take the lock
       | again.
       | 
       | When blocking a thread, Linux (and most conventional operating
       | systems) put the thread into a queue, mark the thread as non-
       | runnable, and yield, and when the thing they're waiting for is
       | ready, they mark one or more queued threads runnable so the
       | executor, I mean scheduler, will run them so they can try again
       | :)
       | 
       | Most OSes do this a lot more slowly than can be done in
       | userspace.
        
         | zozbot234 wrote:
         | > Can anyone (withoutboats?) elaborate on this? If "stackful"
         | coroutines means functions with an ordinary-ish stack that can
         | yield, then those stacks are just memory, and you can take
         | references to them.
         | 
         | See http://www.open-
         | std.org/JTC1/SC22/WG21/docs/papers/2018/p136... for an in-depth
         | analysis. The short version is that stackful fibers turn out to
         | be a terrible choice. The author (Gor Nishanov, for the C++ ISO
         | committee) explicitly says that they should not be used - this
         | is even in spite of the fact that Windows API explicitly
         | supports fibers as a programmimg interface, at a far deeper
         | level than just POSIX setcontext.
        
         | withoutboats3 wrote:
         | The bigger problem is growable stacks. It's true you can
         | suspend a stack and then continue as long as your
         | implementation doesn't ever move it. I didn't really dwell on
         | this because I wrote more about it in "Why async Rust?"
         | 
         | And you're right that the mutex in the kernel is implemented
         | with a similar algorithm - I just meant that the code in the
         | library you're using is very different, because it is a
         | relatively thin wrapper around the relevant syscalls whereas
         | the async one is implemented in userspace.
        
       | 10000truths wrote:
       | The best solution to the problem is to not touch I/O at all in a
       | library (AKA the sans-io philosophy). Everything is instead a
       | state machine, and the user of the library bears the
       | responsibility for gluing the I/O to the state machine's inputs
       | and outputs. This gives you total freedom from execution model -
       | I can run my ssh client or TLS client or HTTP server over epoll,
       | io_uring, kqueue, DPDK, whatever.
        
       ___________________________________________________________________
       (page generated 2024-02-03 23:00 UTC)