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