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