[HN Gopher] Playground Wisdom: Threads Beat Async/Await
       ___________________________________________________________________
        
       Playground Wisdom: Threads Beat Async/Await
        
       Author : samwillis
       Score  : 139 points
       Date   : 2024-11-18 12:07 UTC (5 days ago)
        
 (HTM) web link (lucumr.pocoo.org)
 (TXT) w3m dump (lucumr.pocoo.org)
        
       | NeutralForest wrote:
       | I thought that was interesting and I definitely get the
       | frustration in some aspect. I'm mostly familiar with Python and
       | the function "coloring" issue is so annoying as it forces you to
       | have two APIs depending on async or not (look at SQLAlchemy for
       | example). The ergonomics are bad in general and I don't really
       | like having to deal with, for example, awaiting for a result that
       | will be needed in a sync function.
       | 
       | That being said, some alternatives were mentioned (structured
       | concurrency a la Go) but I'd like to hear about people in BEAM
       | land (Elixir) and what they think about it. Though I understand
       | that for system languages, handling concurrency through a VM is
       | not an option.
        
         | the_mitsuhiko wrote:
         | > structured concurrency a la Go
         | 
         | Go does not have structured concurrency. Goroutines as far as I
         | know don't have much of a relationship with each other at all.
        
           | NeutralForest wrote:
           | My bad, thanks for the correction.
        
         | lbrindze wrote:
         | BEAM very much falls into the same camp as the author's
         | description of Scratch does at the beginning of the article.
         | You have a lot more granular control than Scratch, of course,
         | but it also loosely follows the actor model
        
         | toast0 wrote:
         | Sorry, I kind of spaced on reading the article, but from BEAM
         | land, everything is built around concurrent processes with
         | asynchronous messaging.
         | 
         | You can send a message to something else and wait for the
         | response immediately if you want to write in a more blocking
         | style. And you can write a function that does bot the sending a
         | message and the waiting, so you don't really need to think
         | about it if you don't want to. All I/O pretty much feels the
         | same way, although you get into back pressure with some I/O
         | where if there's a queue you can opt to fail immediately or
         | block your process until the send fits in the queue.
         | 
         | The underlying reality is that your processes don't actually
         | block, BEAM processes are essentialy green threads that are
         | executed by a scheduler (which is an OS thread), so blocking
         | things become yields, _and_ the VM also checks if it should
         | yield at every function call. BEAM is built around functional
         | languages, so it lacks loops and looping is handled by
         | recursive function calls, so a process _must_ make a function
         | call in a finite amount of code, and so BEAM 's green threading
         | is effectively pre-emptive.
         | 
         | The end result of all this is you can spawn as many processes
         | as you like (i've operated systems with one process per client
         | connection, and millions of client connections per node). And
         | you can write most of your code like normal imperitive blocking
         | code. Sometimes you do want to separate out sending messages
         | and receiving responses, and you can easily do that too. This
         | is way nicer than languages with async/await, IMHO; there's no
         | trickyness where calling a blocking function from a async
         | context breaks scheduling, and calling an async function from a
         | non-async context may not be possible... You do still have the
         | possibility of a function blocking when you didn't expect it
         | to, but it will only block the process that called it and
         | transitively, those processes that are waiting for messages
         | from the now blocked process.
         | 
         | Java's Project Loom seems like it will get to a pretty similar
         | place, eventually. But I've seen articles about some hurdles on
         | the way; there's some things that still actually block a thread
         | rather than being (magically) changed to yielding.
         | 
         | Again, IMHO, people didn't build async/await because it is
         | good. They built it because threads were unavailable
         | (Javascript) or to work around the inability to run as many
         | threads as would make the code simple. If you could spawn a
         | million OS threads without worrying about resource use, only
         | constrained languages would have async/await. But OS threads
         | are too heavy to spawn so many, and too heavy to regularly
         | spawn and let die for ephemeral tasks.
        
           | vacuity wrote:
           | To offer a different perspective, green threads are far
           | easier to use for programmers (which is a valuable quality;
           | I'm not dismissing that), but async/await is the better
           | abstraction if you know how to use it. I justify this more in
           | [0]. Async/await is great, but pragmatically it should not be
           | pushed as the sole "asynchronous programming" abstraction. It
           | requires more QoL infrastructure to be even somewhat usable
           | as general-purpose, although I think it'll be worthwhile to
           | continue bridging the gap.
           | 
           | [0] https://news.ycombinator.com/item?id=41900215
        
       | arctek wrote:
       | I actually think out of any language async/await makes the most
       | sense for javascript.
       | 
       | In the first example: there is no such thing as a blocking sleep
       | in javascript. What people use as sleep is just a promise wrapper
       | around a setTimeout call. setTimeout has always created
       | microtasks, so calling a sleep inline would do nothing to halt
       | execution.
       | 
       | I do agree that dangling Promises are annoying and Promise.race
       | is especially bad as it doesn't do what you expect: finish the
       | fastest promise and cancel the other. It will actually eventually
       | resolve both but you will only get one result.
       | 
       | Realistically in JS you write your long running async functions
       | to take an AbortController wrapper that also provides a sleep
       | function, then in your outer loop you check the signal isn't
       | aborted and the wrapper class also handles calling clearTimeout
       | on wrapped sleep functions to stop sleeping/pending setTimeouts
       | and exit your loop/function.
        
         | tempodox wrote:
         | > async/await makes the most sense for javascript.
         | 
         | More like: Has no alternative. There are no threads in JS.
        
           | threadthis wrote:
           | Yes there are. They are called "Workers" (WebWorkers),
           | though. It's a semantic game with a different API but the
           | same concept.
           | 
           | https://www.w3.org/TR/2021/NOTE-workers-20210128/
        
           | com2kid wrote:
           | Note that the underlying C++ libraries for Node are perfectly
           | capable of using threads, only the final user space model is
           | single threaded by default.
        
       | serbuvlad wrote:
       | As someone who has only written serious applications in single-
       | threaded, or manually threaded C/C++, and concurrent applications
       | in go using goroutines, channels, and all that fun stuff, I
       | always find the discussion around async/await fascinating.
       | Especially since it seems to be so ubiquitous in modern
       | programming, outside of my sphere.
       | 
       | But one thing is: I don't get it. Why can't I await in a normal
       | function? await sounds blocking. If async functions return
       | promises, why can't I launch multiple async functions, then await
       | on each of them, in a non-async function that does not return a
       | promise?
       | 
       | I get there are answers to my questions. I get await means "yeald
       | if not ready" and if the function is not async "yeald" is
       | meaningless. But I find it a very strange way of thinking
       | nonetheless.
        
         | avandekleut wrote:
         | At least in node, its because the runtime is an event loop.
        
         | mrkeen wrote:
         | You can:
         | 
         | https://hackage.haskell.org/package/async-2.2.5/docs/Control...
         | 
         | As long as you don't mind - what did the article say? -
         | 
         | >> transcending to a higher plane and looking down to the folks
         | who are stitching together if statements, for loops, make side
         | effects everywhere, and are doing highly inappropriate things
         | with IO.
        
         | mr_coleman wrote:
         | In C# you can do a collection of Task<T>, start them and then
         | do a Task.WaitAll() on the collection. For example a batch of
         | web requests at the same time and then collect the results once
         | everything is done. I'm not sure how it's done in other
         | languages but I imagine there's something similar.
        
         | tbrownaw wrote:
         | The `await` keyword means "turn the rest of this function into
         | a callback for the when the Task I'm waiting on finishes, and
         | return the resulting Task". Returning a Task only works if your
         | function is declared to return a Task.
         | 
         | The `async` keyword flags functions that are allowed to be
         | transformed like that. I assume it could have been made
         | implicit.
         | 
         | You can do a blocking wait on a Task or collection of Tasks.
         | But you don't want to do that from a place that might be called
         | from the event loop's thread pool (such as anything called from
         | a Task's completion callback), since it can lock up.
        
           | captaincrowbar wrote:
           | "The `await` keyword means "turn the rest of this function
           | into a callback for the when the Task I'm waiting on
           | finishes, and return the resulting Task"."
           | 
           | Oh my god thank you. I've been trying to wrap my head around
           | the whole async/await paradigm for years, basically writing
           | code based on a few black magic rules that I only half
           | understand, and you finally made it all clear in once
           | sentence. Why all those other attempts to explain async/await
           | don't just say this I can't imagine.
        
             | xiphias2 wrote:
             | It's because implementing this is not that easy: there are
             | differences between the implementation of coroutines and
             | await that makes it tricky (especially waiting for both CPU
             | tasks and network events).
             | 
             | For Python I loved this talk by David Beazley:
             | 
             | https://www.youtube.com/watch?v=MCs5OvhV9S4&t=2510s
             | 
             | He's implementing async/await from coroutines ground up by
             | live coding on the stage
        
             | quectophoton wrote:
             | > Why all those other attempts to explain async/await don't
             | just say this I can't imagine.
             | 
             | On one hand, the people who write explanations are usually
             | those who have lived through a language's history and
             | learned things gradually, and also have context of how
             | things were solved (or not) before a feature was
             | introduced.
             | 
             | On the other hand, the people who are looking for an
             | explanation, are usually just jumping directly to the
             | latest version of the language, and lack a lot of that
             | context.
             | 
             | Then those who do the explaining underestimate how much
             | their explanations are relying on that prior context.
             | 
             | That's why I think the ideal person to explain something to
             | you, is someone with an experience as similar as possible
             | to your own, but also knows about the thing that you want
             | to know about. Because this way they will use terms similar
             | to those you would use yourself. Even if those terms are
             | imprecise or technically incorrect, they would still help
             | you in actually understanding, because those explanations
             | would actually be building on top of your current
             | knowledge, or close to it.
             | 
             | This is also why all monad explanations are... uh... _like
             | that_.
        
           | ivanjermakov wrote:
           | > I assume it could have been made implicit
           | 
           | Not quite. It gets ambiguous whether to wrap return or not.
           | Example:                   function foo(): Promise<number> {
           | if (...) { return Promise.resolve(5) }             ...
           | }
           | 
           | but async version is:                   async function foo():
           | Promise<number> {             if (...) { return 5 }
           | ...         }
           | 
           | Although you can bake into the language one way or another.
        
         | Rohansi wrote:
         | `await` is only logically blocking. Internally the code in an
         | async function is split up between each `await` so that each
         | fragment can be called separately. They are cooperatively
         | scheduled so `await` is sugar for 1) ending a fragment, 2)
         | registering a new fragment to run when X completes, and 3)
         | yielding control back to the scheduler. None of this internal
         | behavior is present for non-async functions - in C# they run
         | directly on bare threads like C++.
         | 
         | Go's goroutines are comparable to async/await but everything is
         | transparent. In that case it's managed by the runtime instead
         | of a bit of syntactic sugar + libraries.
        
           | neonsunset wrote:
           | Goroutines are also more limited from UX perspective because
           | each goroutine is a true green thread with its own virtual
           | stack. This makes spawning goroutines much more expensive
           | (asynchronously yielding .NET tasks start at just about 100B
           | of allocated memory), goroutines are also very difficult to
           | compose and they cannot yield a value requiring you to
           | emulate task/promise-like behavior by hand by passing it over
           | a channel or writing it to a specific location, which is more
           | expensive.
        
         | shepherdjerred wrote:
         | At least in JavaScript, you could mark all of your functions as
         | `async`.
         | 
         | This would mean that function would have to return a Promise
         | and go back to the event loop which would add overhead. I
         | imagine it'd kill performance since you'd essentially be
         | context switching on every function call.
         | 
         | The obvious workaround for this is to say "I want some of my
         | code to run serially without promises", which is essentially is
         | asking for a `sync` keyword (or, `async` which would be the
         | inverse).
        
         | AlienRobot wrote:
         | In my experience with web browsers, you can't do this because
         | Javascript can NEVER block. For example, if a function takes
         | too long to run, it blocks rendering of the page. If there were
         | ways to make Javascript asynchronously, browsers would have
         | implemented it already, so I assume they can't do it without
         | potential backward incompatibility.
         | 
         | One exception is alert(), which blocks and shows a dialog. But
         | I don't think I've ever seen a website use it instead of
         | showing a "normal" popup with CSS. It looks ugly so it's only
         | used to debug that code actually runs.
         | 
         | I'm not knowledgeable about low-level interruptions, but I
         | think you would need at least some runtime code to implement
         | blocking the thread. In any case, even if the language provides
         | this, you can't use it because the main thread is normally a
         | GUI thread that can't respond to user interaction if it's
         | blocked by another thread. That's the main point of using
         | (background) threads in the first place: so the main thread
         | never blocks from IO bottlenecks.
        
         | akira2501 wrote:
         | > Why can't I await in a normal function?
         | 
         | You can. promise.then(callback). If you want the rest of your
         | logic to be "blocking" then the rest of it goes in the
         | callback. the 'then' method itself returns a promise, so you
         | can return that from a non async function, if you like.
         | 
         | > why can't I launch multiple async functions, then await on
         | each of them, in a non-async function that does not return a
         | promise?
         | 
         | Typically? Exception handling semantics. See the difference
         | between Promise.race, Promise.all and Promise.allSettled.
        
         | binary132 wrote:
         | I found it all very confusing until I eventually wrote a little
         | async task scheduler in Lua. Lua has an async / cooperative-
         | coroutine API that is both very simple and easy to express
         | meaningful coroutines with. The API is almost like a sort of
         | system of safer gotos, but in practice it's very much like Go
         | channel receives, if waiting for a value from a channel was how
         | you passed control to the producer side, and instead of a
         | channel, the producer was just a function call that would
         | return the next value every time you passed it control.
         | 
         | What's interesting is that C++20 coroutines have very nearly
         | the same API and semantics as Lua's coroutines. Still haven't
         | taken the time to dive into that, but now that 23 is published
         | and has working ranges, std::generator looks very promising
         | since it's kind of a lazy bridge between coroutines and ranges.
        
         | chrisweekly wrote:
         | yeald -> yield
        
         | MatmaRex wrote:
         | You can await in a normal function in better languages, just
         | not in JavaScript.
        
           | egeozcan wrote:
           | > Why can't I await in a normal function? await sounds
           | blocking
           | 
           | > You can await in a normal function in better languages,
           | just not in JavaScript.
           | 
           | Await, per common definition, makes you wait asynchronously
           | for a Task/Promise. How on earth are you going to "await" for
           | a Promise which also runs on the same thread on a synchronous
           | function? That function needs to be psuedo-async as in
           | "return myPromise.then(() => { /* all fn code here */ }), or
           | you need to use threads, which brings us to the second
           | point...
           | 
           | With the closest thing to threads (workers) in JavaScript and
           | using SharedArrayBuffer and a simple while loop, perhaps
           | (didn't think too much on it), you can implement the same
           | thing with a user defined Promise alternative but then why
           | would you want to block the main thread which usually has
           | GUI/Web-Server code?
        
             | MatmaRex wrote:
             | It seemed to me that the previous poster wanted a way to
             | wait for the result of a promise (in a blocking manner),
             | and I meant that this is available in other languages.
             | You're right that it is not usually spelled "await".
        
         | davnicwil wrote:
         | I get it - you'd like await semantics in a function without
         | having to expose that detail to the caller.
         | 
         | You can't get it directly in javascript but you're only one
         | step away. Just not awaiting your function in the caller
         | 'breaks the chain' so to speak, so that at least the caller
         | doesn't have to be async. That way you can avoid tagging your
         | function as async completely.
         | 
         | Therefore one syntax workaround while still being able to use
         | await semantics would just be to nest this extra level inside
         | your function -- wrap those await calls in an anonymous _inner_
         | function which is tagged async, which you just instantly call
         | _that_ without await, so the function itself doesn 't have to
         | be (and does not return a promise).
        
       | Const-me wrote:
       | The article seems specific to JavaScript, C# is different.
       | 
       | > you cannot await in a sync function
       | 
       | In C# it's easy to block the current thread waiting for an async
       | task to complete, see Task.Wait method.
       | 
       | > since it will never resolve, you can also never await it
       | 
       | In C#, awaiting for things which never complete is not that bad,
       | the standard library has Task.WhenAny() method for that.
       | 
       | > let's talk about C#. Here the origin story is once again
       | entirely different
       | 
       | Originally, NT kernel was designed for SMP from the ground up,
       | supports asynchronous operations on handles like files and
       | sockets, and since NT 3.5 the kernel includes support for thread
       | pool to dispatch IO completions:
       | https://en.wikipedia.org/wiki/Input/output_completion_port
       | 
       | Overlapped I/O and especially IOCP are hard to use directly. When
       | Microsoft designed initial version of .NET, they implemented
       | thread pool and IOCP inside the runtime, and exposed higher-level
       | APIs to use them. Stuff like Stream.BeginRead / Stream.EndRead
       | available since .NET 1.1 in 2003, the design pattern is called
       | Asynchronous Programming Model (APM).
       | 
       | Async/await language feature introduced in .NET 4.5 in 2012 is a
       | thin layer of sugar on top of these begin/end asynchronous APIs
       | which were always there. BTW, if you have a pair of begin/end
       | methods, converting into async/await takes 1 line of code, see
       | TaskFactory.FromAsync.
        
         | zamadatix wrote:
         | Task.Wait() is just using the normal "thread" (in the way the
         | author defines it later) blocking logic to do that in said case
         | but I think the author is trying to talk about pure async/await
         | approaches there as an example of why you still want exactly
         | that kind of non-async "thread" blocking to fall back on for
         | differently colored functions.
         | 
         | Task.WhenAny() is similar to Promise.any()/Promise.race(). I'm
         | not sure this is where the author is focusing attention on
         | though. Regardless if your execution is able to move on and out
         | of that scope those other promises may still never finish or
         | get cleaned up.
        
         | the_mitsuhiko wrote:
         | You're probably right that this is leaning in on JavaScript and
         | Python more, but I did try to make a point that the origin
         | story for this feature is quite a bit different between
         | languages. C# is the originator of that feature, but the
         | implications of that feature in C# are quite different than in
         | for instance JavaScript or Python. But when people have a
         | discussion about async/await it often loses these nuances very
         | quickly.
         | 
         | > Async/await language feature introduced in .NET 4.5 in 2012
         | is a thin layer of sugar on top of these begin/end asynchronous
         | APIs which were always there.
         | 
         | You are absolutely right. That said, it was a conscious
         | decision to keep the callback model and provide "syntactic
         | sugar" on top of it to make it work. That is not the only model
         | that could have been chosen.
        
           | cwills wrote:
           | Seems like this article conflates threads C# with
           | asynchronous operations a little.
           | 
           | The way I see it, threads are for parallel & concurrent
           | execution of CPU-bound workloads, across multiple CPU cores.
           | And typically use Task Parallel Library. Async/await won't
           | help here.
           | 
           | Whereas async/await for IO bound workloads, and freeing up
           | the current CPU thread until the IO operation finishes. As
           | mentioned, syntactic sugar on top of older callback-based
           | asynchronous APIs.
        
             | the_mitsuhiko wrote:
             | I would make the argument it does not matter what the
             | intention is, in practice people await CPU bound tasks all
             | the time. In fact, here is what the offical docs[1] say:
             | 
             | > You could also have CPU-bound code, such as performing an
             | expensive calculation, which is also a good scenario for
             | writing async code.
             | 
             | [1]: https://learn.microsoft.com/en-
             | us/dotnet/csharp/asynchronous...
        
         | throwitaway1123 wrote:
         | > In C#, awaiting for things which never complete is not that
         | bad, the standard library has Task.WhenAny() method for that.
         | 
         | It's not that bad in JS either. JS has both Promise.any and
         | Promise.race that can trivially set a timeout to prevent a
         | function from waiting infinitely for a non-resolving promise.
         | And as someone pointed out in the Lobsters thread, runtimes
         | that rely on multi-threading for concurrency are also often
         | prone to deadlocks and infinite loops [1].
         | import { setTimeout } from 'node:timers/promises'
         | const neverResolves = new Promise(() => {})              await
         | Promise.any([neverResolves, setTimeout(0)])       await
         | Promise.race([neverResolves, setTimeout(0)])
         | console.trace()
         | 
         | [1]
         | https://lobste.rs/s/hlz4kt/threads_beat_async_await#c_cf4wa1
        
           | cyberax wrote:
           | > Promise.race
           | 
           | Ding! You now have a memory leak! Collect your $200 and
           | advance two steps.
           | 
           | Promise.race will waste memory until _all_ of its promises
           | are resolved. So if a promise never gets resolved, it will
           | stick around forever.
           | 
           | It's braindead, but it's the spec:
           | https://github.com/nodejs/node/issues/17469
        
             | throwitaway1123 wrote:
             | This doesn't even really appear to be a flaw in the
             | Promise.race implementation [1], but rather a natural
             | result of the fact that native promises don't have any
             | notion of manual unsubscription. Every time you call the
             | then method on a promise and pass in a callback, the JS
             | engine appends the callback to the list of "reactions" [2].
             | This isn't too dissimilar to registering a ton of event
             | listeners and never calling `removeEventListener`.
             | Unfortunately, unlike events, promises don't have any
             | manual unsubscription primitive (e.g. a hypothetical
             | `removePromiseListener`), and instead rely on automatic
             | unsubscription when the underlying promise resolves or
             | rejects. You can of course polyfill this missing behavior
             | if you're in the habit of consistently waiting on
             | infinitely non-settling promises, but I would definitely
             | like to see TC39 standardize this [3].
             | 
             | [1] https://issues.chromium.org/issues/42213031#comment5
             | 
             | [2] https://github.com/nodejs/node/issues/17469#issuecommen
             | t-349...
             | 
             | [3] https://github.com/cefn/watchable/tree/main/packages/un
             | promi...
        
               | kaoD wrote:
               | This isn't actually about removing the promise
               | (completion) listener, but the fact that promises are not
               | cancelable in JS.
               | 
               | Promises in JS always run to completion, whether there's
               | a listener or not registered for it. The event loop will
               | always make any existing promise progress as long as it
               | can. Note that "existing" here does not mean it has a
               | listener, nor even whether you're holding a reference to
               | it.
               | 
               | You can create a promise, store its reference somewhere
               | (not await/then-ing it), and it will still progress on
               | its own. You can await/then it later and you might get
               | its result instantly if it had already progressed on its
               | own to completion. Or even not await/then it at all -- it
               | will still progress to completion. You can even not store
               | it anywhere -- it will still run to completion!
               | 
               | Note that this means that promises will be held until
               | completion even if userspace code does not have any
               | reference to it. The event loop is the actual owner of
               | the promise -- it just hands a reference to its
               | completion handle to userspace. User code never "owns" a
               | promise.
               | 
               | This is in contrast to e.g. Rust promises, which do not
               | run to completion unless someone is actively polling
               | them.
               | 
               | In Rust if you `select!` on a bunch of promises (similar
               | to JS's `Promise.race`) as soon as any of them completes
               | the rest stop being polled, are dropped (similar to a
               | destructor) and thus cancelled. JS can't do this because
               | (1) promises are not poll based and (2) it has no
               | destructors so there would be no way for you to specify
               | how cancellation-on-drop happens.
               | 
               | Note that this is a design choice. A tradeoff.
               | Cancellation introduces a bunch of problems with promise
               | cancellation safety even under a GC'd language (think
               | e.g. race conditions and inconsistent internal state/IO).
               | 
               | You can kinda sorta simulate cancellation in JS by
               | manually introducing some `isCancelled` variable but you
               | still cannot act on it except if you manually check its
               | value between yield (i.e. await) points. But this is just
               | fake cancellation -- you're still running the promise to
               | completion (you're just manually completing early). It's
               | also cumbersome because it forces you to check the
               | cancellation flag between each and every yield point, and
               | you cannot even cancel the inner promises (so the inner
               | promises will still run to completion until it reaches
               | your code) unless you somehow also ensure all inner
               | promises are cancelable and create some infra to cancel
               | them when your outer promise is cancelled (and ensure all
               | inner promises do this recursively until then inner-est
               | promise).
               | 
               | There are also cancellation tokens for some promise-
               | enabled APIs (e.g. `AbortController` in `fetch`'s
               | `signal`) but even those are just a special case of the
               | above -- their promise will just reject early with an
               | `AbortError` but will still run to (rejected) completion.
               | 
               | This has some huge implications. E.g. if you do this in
               | JS...                 Promise.race([
               | deletePost(),         timeout(3000),       ]);
               | 
               | ...`deletePost` can still (invisibly) succeed in 4000
               | msecs. You have to manually make sure to cancel
               | `deletePost` if `timeout` completes first. This is
               | somewhat easy to do if `deletePost` can be aborted (via
               | e.g. `AbortController`) even if cumbersome... but more
               | often than not you cannot really cancel inner promises
               | unless they're explicitly abortable, so there's no way to
               | do true userspace promise timeouts in JS.
               | 
               | Wow, what a wall of text I just wrote. Hopefully this
               | helps someone's mental model.
        
               | rerdavies wrote:
               | But if you really truly need cancel-able promises, it's
               | just not that difficult to write one. This seems like A
               | Good Thing, especially since there are several different
               | interpretations of what "cancel-able" might mean (release
               | the completion listeners into the gc, reject based on
               | polling a cancellation token, or both). The javascript
               | promise provides the minimum language implementation upon
               | which more elaborate Promise implementations can be
               | constructed.
        
               | kaoD wrote:
               | Why this isn't possible is implicitly (well, somewhat
               | explicitly) addressed in my comment.
               | const foo = async () => {         ... // sync stuff A
               | await someLibrary.expensiveComputation()         ... //
               | sync stuff B       }
               | 
               | No matter what you do it's impossible to cancel this
               | promise unless `someLibary` exposes some way to cancel
               | `expensiveComputation`, and you somehow expose a way to
               | cancel it (and any other await points) and any other
               | promises it uses internally also expose cancellation and
               | they're all plumbed to have the cancellation propagated
               | inward across all their await points.
               | 
               | Unsubscribing to the completion listener is never enough.
               | Implementing cancellation in your outer promise is never
               | enough.
               | 
               | > The javascript promise provides the minimum language
               | implementation upon which more elaborate Promise
               | implementations can be constructed.
               | 
               | I'll reiterate: there is no way to write promise
               | cancellation in JS userspace. It's just not possible (for
               | all the reasons outlined in my long-ass comment above).
               | No matter how elaborate your implementation is, you need
               | collaboration from every single promise that might get
               | called in the call stack.
               | 
               | The proposed `unpromise` implementation would not help
               | either. JS would need all promises to expose a sort of
               | `AbortController` that is explicitly connected across all
               | cancellable await points inwards which would introduce
               | cancel-safety issues.
               | 
               | So you'd need something like this to make promises
               | actually cancelable:                 const cancelableFoo
               | = async (signal) => {         if (signal.aborted) {
               | throw new AbortError()         }              ... // sync
               | stuff A              if (signal.aborted) {           //
               | possibly cleanup for sync stuff A           throw new
               | AbortError()         }              await
               | someLibrary.expensiveComputation(signal)              if
               | (signal.aborted) {           // possibly cleanup for sync
               | stuff A           throw new AbortError()         }
               | ... // sync stuff B              if (signal.aborted) {
               | // possibly cleanup for sync stuff A           //
               | possibly cleanup for sync stuff B           throw new
               | AbortError()         }       }            const
               | controller = new AbortController()       const signal =
               | abortController.signal            Promise.cancelableRace(
               | controller, // cancelableRace will call
               | controller.abort() if any promise completes         [
               | cancellableFoo(signal),           deletePost(signal),
               | timeout(3000, signal),         ]       )
               | 
               | And you need all promises to get their `signal` properly
               | propagated (and properly handled) across the whole call
               | stack.
        
               | throwitaway1123 wrote:
               | > This isn't actually about removing the promise
               | (completion) listener, but the fact that promises are not
               | cancelable in JS.
               | 
               | You've made an interesting point about promise
               | cancellation but it's ultimately orthogonal to the Github
               | issue I was responding to. The case in question was one
               | in which a memory leak was triggered specifically by
               | racing a long lived promise with another promise -- not
               | simply the existence of the promise -- but specifically
               | racing that promise against another promise with a
               | shorter lifetime. You shouldn't have to cancel that long
               | lived promise in order to resolve the memory leak. The
               | user who created the issue was creating a promise that
               | resolved whenever the SIGINT signal was received. Why
               | should you have to cancel this promise early in order to
               | tame the memory usage (and only while racing it against
               | another promise)?
               | 
               | As the Node contributor discovered the reason is because
               | semantically `Promise.race` operates similarly to this
               | [1]:                 function race<X, Y>(x:
               | PromiseLike<X>, y: PromiseLike<Y>) {         return new
               | Promise((resolve, reject) => {           x.then(resolve,
               | reject)           y.then(resolve, reject)         })
               | }
               | 
               | Assuming `x` is our non-settling promise, he was able to
               | resolve the memory leak by monkey patching `x` and
               | replacing its then method with a no-op which ignores the
               | resolve and reject listeners: `x.then = () => {};`. Now
               | of course, ignoring the listeners is obviously not ideal,
               | and if there was a native mechanism for removing the
               | resolve and reject listeners `Promise.race` would've used
               | it (perhaps using `y.finally()`) which would have solved
               | the memory leak.
               | 
               | [1] https://github.com/nodejs/node/issues/17469#issuecomm
               | ent-349...
        
               | kaoD wrote:
               | > Why should you have to cancel this promise early in
               | order to tame the memory usage (and only while racing it
               | against another promise)?
               | 
               | In the particular case you linked to, the issue is
               | (partially) solved because the promise is short-lived so
               | the `then` makes it live longer, exacerbating the issue.
               | By not then-ing the GC kicks earlier since nothing else
               | holds a reference to its stack frame.
               | 
               | But the underlying issue is lack of cancellation, so if
               | you race a long-lived resource-intensive promise against
               | a short-lived promise, the issue would still be there
               | regardless of listener registration (which admittedly
               | makes the problem worse).
               | 
               | Note that this is still relevant because it means that
               | the problem can kick in in the "middle" of the async
               | function (if any of the inner promises is long) while the
               | `then` problem (which the "middle of the promise" is a
               | special case of "multiple thens", since each await point
               | is isomorphic to calling `then` with the rest of the
               | function).
               | 
               | Without proper cancellation you only solve the particular
               | case if your issue is the latest body of the `then`
               | chain.
               | 
               | (Apologies for the unclear explanation, I'm on mobile and
               | on the vet's waiting room, I'm trying my best.)
        
               | GoblinSlayer wrote:
               | For that matter C# has Task.WaitAsync, so waited task
               | continues to the waiter task, and your code subscribes to
               | the waiter task, which unregisters your listener after
               | firing it, so memory leak is limited to the small waiter
               | task that doesn't refer anything after timeout.
        
         | User23 wrote:
         | > Originally, NT kernel was designed for SMP from the ground
         | up, supports asynchronous operations on handles like files and
         | sockets, and since NT 3.5 the kernel includes support for
         | thread pool to dispatch IO completions:
         | https://en.wikipedia.org/wiki/Input/output_completion_port
         | 
         | Say what you will about Microsoft in that era (and there's a
         | lot to be said), the NT kernel team absolutely crushed it for
         | their customers' use cases. IOCP were years ahead of anything
         | else.
         | 
         | I pretty much hated all of the userspace Win32 work I did
         | (MIDL, COM, DCOM, UGGGGGGGGH), but the Kernel interfaces were
         | wonderful to code against. To this day I have fond memories of
         | Jeffrey Richter's book.
        
           | wbl wrote:
           | It's not enough to have a nicish abstraction, how did it work
           | in practice and eek out performance? I've heard Bryan
           | Cantrell say there wasn't much there and would be curious to
           | really know what the truth is and more explanation on both
           | sides.
        
       | RantyDave wrote:
       | Almost as an aside the article makes an interesting point: memory
       | accesses can block. Presumably if it blocks because it's
       | accessing a piece of hardware the operating system schedules
       | another thread on that core ... but what if it blocks on a
       | 'normal' memory access? Does it stall the core entirely? Can
       | 'hyperthreading' briefly run another thread? Does out of order
       | execution make it suddenly not a problem? Surely it doesn't go
       | all the way down to the OS?
        
         | the_mitsuhiko wrote:
         | > but what if it blocks on a 'normal' memory access? Does it
         | stall the core entirely?
         | 
         | You won't be able to suspend a virtual thread, so that OS
         | thread is going to be blocked no matter what. As far as kernel
         | threads are concerned I think in practice when a page fault
         | happens the kernel yields and lets another thread take over.
        
         | anonymoushn wrote:
         | Hyperthreading is a feature where a single core can process two
         | unrelated instruction streams (i.e. two threads) which is
         | useful for software that executes few instructions per cycle.
        
         | magicalhippo wrote:
         | > what if it blocks on a 'normal' memory access?
         | 
         | If the CPU gotta wait for memory it's gotta wait, and so it
         | just won't make progress. Though we typically say that the CPU
         | has stalled.
         | 
         | How long depends on if it's found in one of the caches, they're
         | progressively slower, or main memory.
         | 
         | All the fancy techniques like out of order execution,
         | speculative execution and hyperthreads are mainly there to
         | trigger memory reads as soon as possible to reduce how long it
         | is stalled.
         | 
         | Some nice detailed SE answer here[1] with some details.
         | 
         | [1]: https://electronics.stackexchange.com/a/622912
        
       | unscaled wrote:
       | I think most of the arguments in this essay rely on this single
       | premise: "The second thing I want you to take away is that
       | imperative languages are not inferior to functional ones."
       | 
       | There is an implied assumption that async/await is a "functional
       | feature" that was pushed into a bunch of innocent imperative
       | languages and polluted them. But there is one giant problem with
       | this assumption: async/await is not a functional feature. If
       | anything, it's the epitome of an imperative flow-control feature.
       | 
       | There are many kinds of functional languages out there, but I
       | think the best common denominator for a primarily functional
       | language nowadays is exactly this: in functional languages
       | control flow structures are first class citizens, and they can be
       | customized by the programmer. In fact, most control flow
       | structures are basically just functions, and the one that aren't
       | (e.g. pattern matching in ML-like languages and monadic
       | comprehensions in Haskell-inspired languages) are extremely
       | generic, and their behavior depends on the types you feed into
       | them. There are other emphasis points that you see in particular
       | families of languages such as pattern matching, strict data
       | immutability or lazy computation -- but none of these is a core
       | functional concept.
       | 
       | The interesting point I want to point out is that no primarily
       | functional language that I know actually has async/await. Some of
       | them have monads and these monads could be used for something
       | like async/await but that's not a very common use, and monad
       | comprehensions can be used for other things. For instance, you
       | could use do expressions in Haskell (or for expressions in Scala)
       | to operate on multiple lists at once. The same behavior is
       | possible with nested for-loops in virtually every modern
       | imperative language, but nobody has blamed Algol for "polluting"
       | the purity of our Fortran gotos and arithmetic ifs with this
       | "fancy functional garbage monad from damned ivory tower
       | Academia". That would be absurd, not only because no programming
       | language with monadic comprehensions existed back then, but also
       | because for loops are a very syntax for a very specific thing
       | that can be done with monadic expression. They turn a very
       | abstract functional concept into a highly specific -- and highly
       | _imperative_ -- feature. The same is true for await. It 's an
       | imperative construct that instructs the runtime to suspend (or
       | the compiler to turn the current function into a state machine).
       | 
       | So no, async/await does not have anything to do with functional-
       | language envy and is, in fact, a feature that is quite
       | antithetical to functional programming. If there is any
       | theoretical paradigm behind async/await (vs. just using green
       | threads), it's strong typing and especially the idea of
       | representing effects by types. This is somewhat close to fully-
       | fledged Effect Systems (in languages such as a Koka), but not as
       | powerful. The general idea is that certain functions behave in a
       | way that is "infective" -- in other words, if foo() calls bar()
       | which in-turn calls doStuff(), it might be impacted by some side-
       | effect of doStuff(). In order to prevent unpleasant surprises, we
       | want to mark this thing that doStuff does in the function
       | signature (either using an extra argument, a return type wrapper
       | or just an extra modifier like "async").
       | 
       | In a pure language like Haskell, everything from I/O to mutable
       | memory requires specifying an effect and this is usually done
       | through monadic return types. But even the very first version of
       | Java (Ron Pressler's ideal untarnished "imperative" language) has
       | effects (or "colors") which still remain in the language: checked
       | exceptions. They are just as infective as async I/O. If you don't
       | handle exceptions in place, a function marked with "throws
       | IOException" (basically almost any function that deals with I/O)
       | can only be called by another function marked with "throws
       | IOException". What's worse, unlike JavaScript which only has two
       | colors (async and non-async), Java has an infinite number colors!
       | 
       | The description above sounds horrible, but it's not. Checked
       | exceptions are widely believed to be a mistake[1], but they don't
       | bother Java developers enough to make the language unusable. You
       | can always just wrap them with another exception and rethrow. The
       | ergonomics could have been made slightly better, but they're
       | decent enough. But the same can be said for async/await. If you
       | take a language with a similar feature that is close to Java (C#
       | or Kotlin), you'll see the asynchronous functions can still run
       | as blocking code from inside synchronous functions, while
       | synchronous functions can be scheduled on another thread from a
       | synchronous function. The ergonomics for doing that are not any
       | harder than wrapping exceptions.
       | 
       | In addition to that, the advantages of marking a function that
       | runs asynchronous I/O (just like marking a function that throws
       | an exception) are obvious, even if the move itself is
       | controversial. These functions generally involve potentially slow
       | network I/O and you don't want to call them by mistake. If you
       | think that never happens, here is the standard Java API for
       | constructing an InetAddress object from a string representing an
       | IPv4 or IPv6 address: InetAddress.getByName()[2]. Unfortunately,
       | if your IP address is invalid, this function may block while
       | trying to resolve it as a domain name. That's plain bad API
       | design, but APIs that can block in surprising ways are abundant,
       | so you cannot argue that async/await doesn't introduce additional
       | safety.
       | 
       | But let's face it -- in most cases choosing async/await vs. green
       | threads for an imperative language is a matter of getting the
       | right trade-off. Async/Await schedulers are easier to implement
       | (they don't need to deal with segmented/relocatable/growable
       | stacks) and do not require runtime support. Async/await also
       | exhibits more efficient memory usage, and arguably better
       | performance in scenarios that do not involve a long call-graph of
       | async functions. Async/await schedulers also integrates more
       | nicely with blocking native code that is used as a library (i.e.
       | C/C++, Objective C or Rust code). With green threads, you just
       | cannot run this code directly from the virtual thread and if the
       | code is blocking, your life becomes even harder (especially if
       | you don't have access to kernel threads). Even with full control
       | of the runtime, you'd usually end up with a certain amount of
       | overhead for native calls[3].
       | 
       | Considering these trade-offs, async/await is perfect in scenarios
       | like below:
       | 
       | 1. JavaScript had multiple implementations. Not only were most of
       | them single-threaded, they would also need a major overhaul to
       | support virtual threads even if a thread API was specified.
       | 
       | 2. Rust actually tried green threads and abandoned them. The
       | performance was abysmal for a language that seeks zero-cost
       | abstraction and the system programming requirements for Rust made
       | them a deal breaker even if this wasn't the case. Rust just had
       | to support pluggable runtimes and mandating dynamic stacks just
       | won't work inside the Kernel or in soft real-time systems.
       | 
       | 3. Swift had to interoperate with a large amount of Objective C
       | called that was already using callbacks for asynchronous I/O
       | (this is what they had). In addition, it is not garbage-collected
       | language, and it still needed to call a lot of C and Objective C
       | APIs, even if that was wrapped by nice Swift classes.
       | 
       | 4. C# already had a Promise-like Task mechanism that evolved
       | around wrapping native windows asynchronous I/O. If .Net was
       | redesigned from scratch nowadays, they could have very well went
       | with green threads, but the way .Net developed, this would have
       | just introduced a lot of compatibility issues for almost no
       | gains.
       | 
       | 5. Python had the GIL, as the article already mentioned. But even
       | with patching runtime I/O functions (like greenlet -- or more
       | accurately, gevent[4] -- did), there were many third party
       | libraries relying on native code. Python just went with the more
       | compatible approach.
       | 
       | 6. Java did not have any established standard for asynchronous
       | I/O. CompletableFuture was introuced in Java 8, but it wasn't as
       | widely adopted (especially in the standard library) as the C#
       | Task was. Java also had gauranteed full control of the runtime
       | (unlike JavaScript and Rust), it was garbage collected (unlike
       | Rust and Swift) and it had less reliance on native code than
       | Swift, Pre-.NET Core C# or Python. On the other hand, Java had a
       | lot of crucial blocking APIs that haven't been updated to use
       | CompletableFuture, like JDBC and Servlet (Async Servlets were
       | cumbersome and never caught on). Introducing async/await to Java
       | would mean having to rewrite or significantly refactor all
       | existing frameworks in order to support them. That was not a very
       | palatable choice, so again, Java did the correct thing and went
       | with virtual threads.
       | 
       | If you look at all of these use cases, you'd see all of these
       | languages seem to have made the right pragmatic choice. Unless
       | you are designing a new language from scratch (and that language
       | is garbage collected and doesn't need to be compatible with
       | another language or deal with a lot of existing native code), you
       | can go with the ideological argument of "I want my function to be
       | colorless" (or, inversely, you can go with the ideological
       | argument of "I want all suspending functions to be marked
       | explicitly"). In all other cases, pragmatism should win.
       | 
       | ---
       | 
       | [1] Although it mostly comes to bad composability -- checked
       | result types work very well in Rust.
       | 
       | [2]
       | https://docs.oracle.com/en/java/javase/17/docs/api/java.base...
       | 
       | [3] See the article blelow for the overhead in Go. Keep in mind
       | that the Go team has put a lot of effort into optimizing Cgo
       | calls and reducing this overhead, but they still cannot eliminate
       | it entirely. https://shane.ai/posts/cgo-performance-in-go1.21/
       | 
       | [4] https://www.gevent.org/
        
         | the_mitsuhiko wrote:
         | > There is an implied assumption that async/await is a
         | "functional feature" that was pushed into a bunch of innocent
         | imperative languages and polluted them. But there is one giant
         | problem with this assumption: async/await is not a functional
         | feature. If anything, it's the epitome of an imperative flow-
         | control feature.
         | 
         | async/await comes from C# and C# got this as an "appoximation"
         | of what was possible with F#. You can go back to 2011 where
         | there are a series of videos on Channel 9 by Anders Hejlsberg
         | where he goes into that.
         | 
         | That said, I don't think my post relies on the premise that
         | this is a fight about imperative to functional programming. If
         | anything the core premise is that there is value in being able
         | to yield anywhere, and not just at await points.
         | 
         | > If you look at all of these use cases, you'd see all of these
         | languages seem to have made the right pragmatic choice.
         | 
         | Potentially, who am I to judge. However that choice was made at
         | a certain point in time and the consequences are here to stay.
         | Other than in JavaScript where it's self evident that this is a
         | great improvement over promise chaining (sans the challenge of
         | unresolved promises), I'm not sure the benefits are all that
         | evident in all languages. I do a fair amount of async
         | programming in JavaScript, Python and Rust and the interplay
         | between threads and async code is very complex and hard to
         | understand, and a lot of the challenges on a day to day would
         | really feel like they are better solved in the scheduler and
         | virtual threads.
         | 
         | > Unless you are designing a new language from scratch (and
         | that language is garbage collected and doesn't need to be
         | compatible with another language or deal with a lot of existing
         | native code), you can go with the ideological argument of "I
         | want my function to be colorless" (or, inversely, you can go
         | with the ideological argument of "I want all suspending
         | functions to be marked explicitly"). In all other cases,
         | pragmatism should win.
         | 
         | I will make the counter argument: even in some languages with
         | async/await like Python, you could very pragmatically implement
         | virtual threads. At the end of the day in Python for instance,
         | async/await is already implemented on top of coroutines
         | anyways. The "only" thing that this would require, is to come
         | to terms with the idea that the event loop/reactor would have
         | to move closer to the core of the language. I think on a long
         | enough time horizon Python would actually start moving towards
         | that, particularly now that the GIL is going and that the
         | language is quite suffering from the complexities of having two
         | entirely incompatible ecosystems in one place (two sets of
         | future systems, two sets of synchronization directives, two
         | independent ways to spawn real threads etc.).
        
           | ikekkdcjkfke wrote:
           | I always instantly await all my Tasks/promises. The only
           | problem I have is that I have to type it out and wrap
           | everything in Tasks. If the compiler would just automatically
           | asyncify all the things that would be great, no need for
           | green-threading or messing around with the engine, just hide
           | the ugly details like you do with so many other things, dear
           | compiler.
        
         | solidninja wrote:
         | Thank you for writing this - it is more detailed that I could
         | come up with!
         | 
         | I would like to add that I feel like functional approaches are
         | more the "future" of programming than trying to iterate over
         | imperative ones to make them as "nice" to use. So I don't
         | really see the big deal of trying to add-on features to
         | existing languages when you can adopt new ones (or experiment
         | with existing ones e.g. https://github.com/getkyo/kyo for a new
         | take on effects in Scala).
        
         | brabel wrote:
         | > What's worse, unlike JavaScript which only has two colors
         | (async and non-async), Java has an infinite number colors!
         | 
         | Your comment is great, but I need to point out that the above
         | sentence is misrepresenting Java. You can call any function
         | from a Java function. The fact that you may need to handle an
         | Exception when calling some doesn't make it a "colored"
         | function because you can easily handle the Exception and forget
         | about the color, and if you remember the color problem, it was
         | problematic that colors are infectious, i.e. you just can't get
         | rid of the color, which is not the case in Java. Some claim
         | that's actually bad because it prevents things like structured
         | concurrency (because Java can start a Thread anywhere and
         | there's no way for you to know that a function won't... if
         | there was a "color", or better said, effect, for starting a
         | Thread, you could guarantee that no Thread would be started by
         | a function lacking that effect).
        
         | dfawcus wrote:
         | On point [3], using the Shane's code, and with an additional
         | one for gccgo, on my laptop I see:                   $ go-11
         | test -cpu=1,2,4,8,16 -bench Cgo         goos: linux
         | goarch: amd64         pkg: github.com/shanemhansen/cgobench
         | cpu: 13th Gen Intel(R) Core(TM) i5-1340P
         | BenchmarkCgoCall         6123471        195.8 ns/op
         | BenchmarkCgoCall-2      11794101         97.74 ns/op
         | BenchmarkCgoCall-4      22250806         51.30 ns/op
         | BenchmarkCgoCall-8      33147904         34.16 ns/op
         | BenchmarkCgoCall-16     53388628         22.41 ns/op
         | PASS         ok   github.com/shanemhansen/cgobench 6.364s
         | $ go-11 test -cpu=1,2,4,8,16 -bench Gcc         goos: linux
         | goarch: amd64         pkg: github.com/shanemhansen/cgobench
         | cpu: 13th Gen Intel(R) Core(TM) i5-1340P
         | BenchmarkGccCall        414216266          3.037 ns/op
         | BenchmarkGccCall-2      788898944          1.523 ns/op
         | BenchmarkGccCall-4      1000000000          0.7670 ns/op
         | BenchmarkGccCall-8      1000000000          0.4909 ns/op
         | BenchmarkGccCall-16     1000000000          0.3488 ns/op
         | PASS         ok   github.com/shanemhansen/cgobench 4.806s
         | $ go-11 test -cpu=1,2,4,8,16 -bench EmptyCall         goos:
         | linux         goarch: amd64         pkg:
         | github.com/shanemhansen/cgobench         cpu: 13th Gen Intel(R)
         | Core(TM) i5-1340P         BenchmarkEmptyCallInlineable
         | 1000000000          0.5483 ns/op
         | BenchmarkEmptyCallInlineable-2      1000000000          0.2752
         | ns/op         BenchmarkEmptyCallInlineable-4      1000000000
         | 0.1463 ns/op         BenchmarkEmptyCallInlineable-8
         | 1000000000          0.1295 ns/op
         | BenchmarkEmptyCallInlineable-16     1000000000          0.1225
         | ns/op         BenchmarkEmptyCall                  499314484
         | 2.401 ns/op         BenchmarkEmptyCall-2
         | 977968472          1.202 ns/op         BenchmarkEmptyCall-4
         | 1000000000          0.6316 ns/op         BenchmarkEmptyCall-8
         | 1000000000          0.4111 ns/op         BenchmarkEmptyCall-16
         | 1000000000          0.2765 ns/op         PASS         ok
         | github.com/shanemhansen/cgobench 5.707s
         | 
         | Hence the GccGo version of calling the C function is in the
         | same ballpark as for a native Go function call. This is as to
         | be expected when using that mechanism.
         | 
         | So using various C libraries does not necessarily have to
         | involve the overhead from Cgo.                   diff --git
         | a/bench.go b/bench.go         index 8852c75..7bfd870 100644
         | --- a/bench.go         +++ b/bench.go         @@ -15,3 +15,10
         | @@ func Call() {          func CgoCall() {
         | C.trivial_add(1,2)          }         +         +//go:linkname
         | c_trivial_add trivial_add         +func c_trivial_add(a int, b
         | int) int         +         +func GccCall() {         +
         | c_trivial_add(1,2)         +}         diff --git
         | a/bench_test.go b/bench_test.go         index 9523668..c390c63
         | 100644         --- a/bench_test.go         +++ b/bench_test.go
         | @@ -43,3 +43,6 @@ func BenchmarkEmptyCall(b *testing.B) {
         | func BenchmarkCgoCall(b *testing.B) {                 pbench(b,
         | CgoCall)          }         +func BenchmarkGccCall(b
         | *testing.B) {         +       pbench(b, GccCall)         +}
        
       | andrewstuart wrote:
       | Feels academic because despite the concerns raised, I only
       | experience async/await as a good thing in real world.
        
         | BugsJustFindMe wrote:
         | I don't. Now what? I agree with the author, especially in
         | Python. The core Python developers so lost their minds fleeing
         | from the GIL that they forgot historical lessons about how much
         | more ergonomic preemptive multitasking is vs cooperative.
        
           | smitty1e wrote:
           | > so lost their minds fleeing from the GIL that they forgot
           | historical lessons
           | 
           | I just don't agree. `async def` gets the fact that we've
           | departed Kansas, good Toto, right out front.
           | 
           | Async moves the coder a step in the direction of the
           | operating system itself. The juice is not worth the squeeze
           | unless the project bears fruit.
           | 
           | I hardly do enough networky stuff to make this facility
           | useful to me, but I'm grateful to the hours poured into
           | making it part of Python.
           | 
           | Contra the author of The Famous Article, threads seem
           | gnarlier still, and I would likely architect out any async
           | parts of a system into another service and talk to it over a
           | port.
        
             | BugsJustFindMe wrote:
             | > _I would likely architect out any async parts of a system
             | into another service and talk to it over a port_
             | 
             | This doesn't get you any closer to the goal though. Would
             | you talk to the other service with a blocking socket
             | request or a non-blocking one? A non-blocking socket
             | request either invokes a system thread or asyncio. You
             | can't escape that part.
        
       | whoisthemachine wrote:
       | > Your Child Loves Actor Frameworks
       | 
       | It turns out, Promises _are_ actors. Very simple actors that can
       | have one and only one message that upon resolution they dispatch
       | to all other subscribed actors [0]. So children might love
       | Promises and async /await then?
       | 
       | Personally, I've often thought the resolution to the "color"
       | debate would be for a new language to make all public interfaces
       | between modules "Promises" by default. Then the default
       | assumption is "if I call this public function it could take some
       | time to complete". Everything acting synchronously should be an
       | implementation detail that is nice if it works out.
       | 
       | https://en.wikipedia.org/wiki/Futures_and_promises#Semantics...
        
         | emadda wrote:
         | That's a nice mental model for promises.
         | 
         | But it is not always true that one promise instance can be
         | awaited in multiple places.
         | 
         | In Swift you cannot get the ref to the Promise instance, so you
         | cannot store it or await it at multiple places.
         | 
         | Once you start an async fn the compiler forces you to await it
         | where it was started (you can use `await task.value`, but that
         | is a getter fn that creates a new hidden promise ref on every
         | call).
        
           | whoisthemachine wrote:
           | I'm not familiar with Swift, but it still sounds like it's
           | describing an actor model, just one with a subset of the
           | functionality.
        
           | manmal wrote:
           | If I understand the use case correctly, then, in Swift, Tasks
           | provide exactly what you have described. You can use the Task
           | object in multiple places to await the result.
        
         | marcosdumay wrote:
         | > Personally, I've often thought the resolution to the "color"
         | debate would be for a new language to make all public
         | interfaces between modules "Promises" by default.
         | 
         | Works well on Haskell. You can even remove that "public" and
         | "between modules".
        
       | cryptonector wrote:
       | Threads are definitely not _the_ answer but _an_ answer.
       | 
       | You can have as many threads as hardware threads, but in each
       | thread you want continuation passing style (CPS) or async-await
       | (which is a lot like syntactic sugar for CPS). Why? Because
       | threads let you smear program state over a large stack,
       | increasing memory footprint, while CPS / async-await forces you
       | to make all the state explicit and compressed, thus optimizing
       | memory footprint. This is not a small thing. If you have thread-
       | per-client services, each thread will need a sizeable stack, each
       | stack with a guard page -- even with virtual memory that's
       | expensive, both to set up and in terms of total memory footprint.
       | 
       | Between memory per client, L1/L2 cache footprint per client, page
       | faults (to grow the stack), and context switching overhead,
       | thread-per-client is much more expensive than NPROC threads doing
       | CPS or async-await. If you compress the program state per client
       | you can fit more clients in the same amount of memory, and the
       | overhead of switching from one client to another is lower, thus
       | you can have more clients.
       | 
       | This is the reason that async I/O is the key to solving the
       | "C10K" problem: it forces the programmer to compress per-client
       | program state.
       | 
       | But if you don't need to cater to C10K (or C10M) then thread-per-
       | client is definitely simpler.
       | 
       | So IMO it's really about trade-offs. Does your service need to be
       | C10K? How much are you paying for the hardware/cloud you're
       | running it on? And so on. Being more efficient will be more
       | costly in developer cycles -- that can be _very_ expensive, and
       | that 's the reason that research into async-await is ongoing:
       | hopefully it can make C10K dev cheaper.
       | 
       | But remember, rewrites cost even more than doing it right the
       | first time.
        
         | bvrmn wrote:
         | > Does your service need to be C10K?
         | 
         | It's incorrect question. The correct one "Do your downstream
         | services could handle C10K?" For example a service with a
         | database should almost never be bothered with C10K problem
         | unless most of the requests could skip db access.
         | 
         | Every time you introduce backpressure handling in C10K-ready
         | app it's a red flag you should simply use threads.
        
           | cryptonector wrote:
           | I think you're saying that a database can't be C10K. Why? You
           | don't say but I imagine that you mean because it's I/O bound,
           | not CPU bound. And that may be true, but it may also not be
           | true. Consider an all in-memory database (no paging): it will
           | not be I/O bound.
           | 
           | > Every time you introduce backpressure handling in
           | C10K-ready app it's a red flag you should simply use threads.
           | 
           | That's an admission that threads are slower. I don't see why
           | you wouldn't want ways to express backpressure. You need
           | backpressure for when you have impedance mismatches in
           | performance capabilities; making all parts of your system
           | equally slow instead is not an option.
        
       | mark_l_watson wrote:
       | Nice read, and the article got me to take a look at Java's
       | project Loom and then Eric Normand's writeup on Loom and
       | threading options for Clojure.
       | 
       | Good stuff.
        
       | pwdisswordfishz wrote:
       | > Go, for instance, gets away without most of this, and that does
       | not make it an inferior language!
       | 
       | Yes, it does. Among other things.
        
       | manmal wrote:
       | I view Swift's Tasks as a thread-like abstraction that does what
       | the author is asking for. Not every Task is providing structured
       | concurrency in the strict sense, because cancellation has to be
       | managed explicitly for the default Task constructor. But Tasks
       | have a defined runtime, cancellation, and error propagation, if
       | one chooses to use a TaskGroup, async let, or adds some glue
       | code. The tools to achieve this are all there.
        
       | agentkilo wrote:
       | People should try Janet (the programming language). Its fiber
       | abstraction got everything right IMO.
       | 
       | Functions in Janet don't have "colors", since fiber scheduling is
       | built-in to the runtime in a lower level. You can call "async"
       | functions from anywhere, and Janet's event loop would handle it
       | for you. It's so ergonomic that it almost feels like Erlang.
       | 
       | Janet has something akin to Erlang's supervisor pattern too,
       | which, IMO, is a decent implementation of "structured
       | concurrency" mentioned in the article.
        
       | FlyingSnake wrote:
       | I expected to see Swift but seems like most such discussions
       | overlook it. Here's a great discussion that goes deeper into it:
       | https://forums.swift.org/t/concurrency-structured-concurrenc...
        
       | nextcaller wrote:
       | I'm still not sure if function coloring is also a problem in
       | javascript. The problem became very clear in other languages like
       | python or c#. But in javascript i've been writing code only
       | awaiting a function when I need to and haven't ran into issues. I
       | might write some simple experiment to check myself.
        
         | neonsunset wrote:
         | Why is it not a problem in JavaScript but is one in C#?
        
           | nextcaller wrote:
           | In c#/python you are forced to await the whole chain, the
           | compiler forces you. While in javascript it allows me without
           | a warning. That's why it seems as if things work differently
           | in javascript, if it allows me to (not) use await freely. (I
           | don't remember if c# was just a warning or an error).
        
             | neonsunset wrote:
             | No. It is best to verify assumptions first before
             | presenting them as facts. It is irritating to see this
             | everywhere as the quality of discussion keeps going down.
             | 
             | In order to get the result of a promise in JS you have to
             | await it (or chain it with 'then', much like .NET's
             | 'ContinueWith' although it is usually discouraged).
             | Consumption of tasks in .NET follows a similar pattern.
             | 
             | Async implementations in .NET and Python have also vastly
             | different performance implications (on top of pre-existing
             | unacceptable performance of Python in general).
             | 
             | The analyzer gives you a warning for unawaited tasks where
             | not awaiting task indicates a likely user error.
             | 
             | Which is why if you want to fire and forget a task, you
             | write it like '_ = http.PostAsync(url, content);` - it also
             | indicates the intention clearly.
        
               | nextcaller wrote:
               | Oh sorry for irritating you, I will return to my pod.
        
       ___________________________________________________________________
       (page generated 2024-11-23 23:01 UTC)