8 ms·
I've implemented coroutines in C and C++; my preferred multitasking environment is message-passing between processes. I'm not quite sure what the async/await st
by thechao 2y ago
I've implemented coroutines in C and C++; my preferred multitasking environment is message-passing between processes. I'm not quite sure what the async/await stuff is buying us (I'm thinking C++, here). Like, I get multi-shot stackless coroutines, i.e., function objects, but I don't get why you'd want to orchestrate some sort of temporal Turing pit of async functions bleeding across your code base.
I dunno. Maybe I'm old now?
Anyways; good for Ruby! Async/await just seems very faddish to me: it didn't solve any of the hard multithreading/multiprocessing problems, and introduced a bunch of other issues. My guess is that it was interesting type theory that bled over into Real Life.
- anonymoushn 2y agoIf you have hundreds or thousands of connection-handlers or scripts-attached-to-game-objects, then it can be useful to write code in those without dividing the code into basic blocks each time I/O might be performed or time might pass in the game. I generally agree that manually migrating everything to "be async" in order to achieve this exposes at the very least a lack of belief that computers can be used to automate drudge work.
- mikepurvis 2y agoI really like the patterns that it enables for situations with a lot of parallel IO. Easy example is any kind of scraper, where you're fetching large numbers of pages and then immediately processing those to generate more requests for the links and assets referenced within.
- skywhopper 2y agoMy theory is that JavaScript programmers who were forced into thinking this way for decades with their single-threaded runtime have infected other languages with the idea that this style of coding is not only good but furthermore that it needs to be explicit. Thank goodness we have wiser language developers out there who have resisted this impulse.
- Erem 2y agoDidn’t async/await originate in c#?
- explaininjs 2y agoF# first actually. Then C#. Then Haskell. Then Python. Then TypeScript. Parent just has an axe to grind.
- jerf 2y agoasync/await may have originated as keywords in C#, I don't know, but the programming style itself is ancient. Almost every GUI ever written is programmed in the style. Network servers were written this way back when threads were just a fever dream of academics yet. The entire Mac OS ecosystem prior to OSX had to be written in this style because time was cooperatively shared across the entire OS. This is one of the most blatent "failure to learn from our history" debates I know of, when people act as if the question started with Javascript just a few years ago, when in fact the programming community has experience with this style that predates when most of us were born. I'm getting to old fogey myself and it still predates my 1978 birthdate. I was never even tempted to get into Node because I'd already made an async/await-style mess in Perl before Node was even released, and the mess had everything to do with the programming style and little to do with Perl qua Perl, and the libraries I used were already old and well-established then, if not outright long in the tooth. There is nothing new about this, except keywords.
- explaininjs 2y agoComing from a heavy TS background into a go-forward company, I’d say the main thing you get with async is it makes it incredibly obvious when computation can be performed non-sequentially (async…). For example, It’s very common to see the below in go code: a := &blah{} rA, err := engine.doSomethingWithA() b := &bloop{} rB, err := engine.doSomethingWithB() This might have started out with both the doSomethings being very quick painless procedures. But over time they’ve grown into behemoth network requests and very thing is slow and crappy. No, it’s not exactly hard to spin up a go routine to handle the work concurrently, but it’s not trivial either - and importantly, it’s not immediately obvious that this would be a good idea. Contrast to TS: let a = {blah} let [rA, err] = engine.doSomethingWithA() let b = {bloop} let [rB, err] engine.doSomethingWithB() Now, time passes, you perform that behemoth slowing down of the doSomethings. You are forced by the type system to change this: let a = {blah} let [rA, err] = await engine.doSomethingWithA() let b = {bloop} let [rB, err] await engine.doSomethingWithB() It’s now immediately obvious that you might want to run these two procedures concurrently. Obviously you will need to check the engine code, but any programmer worth their salt should at least seek to investigate concurrency when making that change. I wouldn’t be bringing this up if I hadn’t made 10x+ performance improvements to critical services within a month of starting at a new company in a new language on a service where the handful of experienced go programmers on the team had no idea why their code was taking so long.
- hombre_fatal 2y agoOf course, the other nice thing about the JS example compared to Go is that it's trivial at the callsite to do this: const [[rA, errA], [rB, errB]] = await Promise.all([ engine.doSomethingWithA(), engine.doSomethingWithB() ]) At least these days you can ask an LLM to write the WaitGroup boilerplate for you in Go.
- explaininjs 2y agoIndeed. And breakpoints and stepping across concurrent context actually works in JS, which is nice.
- 2y ago
- DarkNova6 2y ago> Anyways; good for Ruby! Async/await just seems very faddish to me: it didn't solve any of the hard multithreading/multiprocessing problems, and introduced a bunch of other issues. My guess is that it was interesting type theory that bled over into Real Life. I think what happened what that JavaScript absolutely necessitated the addition of async/await to avoid callback hell (due to its single-threaded mandate)... just to create a new kind of hell all of its own. But before the long-term consequences within large code bases could be observed, the cool kids like C# & friends jumped on the bandwagon believing that "more syntax equals more better code".
- incrudible 2y agoJavascript necessitates async because of its focus on UI. You just can not have good UI while blocking on the main (or UI) thread, you will inevitably have something like async and callbacks, so you might as well embrace it. Whether that is a good tradeoff for a server is a different question.
- DarkNova6 2y agoWhat you need is concurrency. And async/await is just one form of concurrency. I don’t see why you would need threads to create a stackful coroutine implementation. However, what you would indeed need is a much more heavy runtime.
- rpeden 2y agoAsync and await appeared in C# well before they were added to JavaScript, so I'm not sure the reasoning of your timeline makes sense.
- sqeaky 2y agoI think the async/await patterns solve one problem really well: UI latency. My UI background is web testing and C++ game UIs in C++. There are like 3 patterns in multithreaded games. Thread per responsibility (old/bad), Barriers blocking all threads and give each game system all the threads, or something smart. Few games do something smart, and the thread per responsibility is not ideal and we will ignore it for now. In games often pausing everything and letting the physics system have all the threads for a few milliseconds is "fast enough". Then the graphics systems will try to use all the threads, and so on so that eventually everything will get all the threads even thought everything else rarely needs it. Sometimes two things are both close to single threaded and have no data contention so they might both be given threads, but this is almost always a manually decided things by experts. This means that once UI, the buttons, the text, cursors, status bars, etc, gets its turn there won't be any race conditions (good), but if it needs to request something from disk that pause will happen on a thread in the UI system (bad and analogous to web sites making web API calls) so UI latency can be a real problem. If any IO small or some resource system has preloaded it then there isn't a detectable slowdown, but there are still plenty of silly periods of waiting. There is also a lot of time when some single threaded part of the game isn't using N-1 hardware threads and all that IO could have been asynchronous. But often game UIs are a frame behind the rest of the game simulation and there is often detectable latency in the UI like the mouse feeling like it drags or similar. Allowing IO to run in the back while active events are processed can reduce latency and this is the default in web browsers. IO latency in web pages is worse than in games and other computation seems smaller than games, so the the event loop is close to ideal. A function is waiting? throw it on the stack and grab something else to do! This means that all the work that can be done while waiting on IO is done and when does well makes a UI snappy. If that were available sensibly in games it could allow a game designed appropriately to span IO across multiple frames and be snappy without stutters. With games using the strategy I described above latency in the game simulation or IO can cause the UI to feel sluggish and vice versa. In games caching UI details and trying to pump the frame rate is "good enough". If the UI is a frame behind but we have 200 frames per second, that isn't really a problem. But when it chugs and the mouse stops responding because the player built the whole game world out of dynamite and set it off the game will not process the mouse until that 30 minutes of physics work is done. There are better scheduling schemes for games. I am a big fan of doing "something smart" but that usually means scheduling heterogenous work with nuanced dependencies and I have written libraries just for that because it isn't actually that hard. But if you don't have the raw compute demands of a game scheduling IO along side your UI computation is often "fast enough" and is any easy enough mental model for JS devs to grok and allow them freedom to speed things up with their own solutions like caching schemes and reworking their UIs.
- kubectl_h 2y agoI work on a large multi-threaded ruby code base and it's always a pain to deal with engineers introducing the async usage in it. Most of the time these engineers don't have a grasp on what fibers are good for and we have to painstakingly review their code and provide feedback that no, it's not magical concurrency, we have a limited number of fix sized connection pools for postgres, redis, memcache, etc available in the process and fibers have to respect those limits as much as threaded code. In fact it's better if you don't introduce any additional concurrency below the thread-per-request model if you can. Only in places like concurrent http calls do we allow some async/fiber usage but even then it ends up nominally looking like threaded code and the VM context switching time saved by using fibers instead of threads is trivial. Trying to use fibers instead of threads in typical CRUD workloads feels a little cargo culty to me. Fibers are cool and performant but usually they should be limited to lightweight work with large concurrency requirements.
- Glyptodon 2y agoI wish there were limited forms of true parallelism available with fibers instead of it just being another concurrency construct limited by an interpreter lock. I feel like there should be an in-language construct for stuff that's safely parallel and narrow, and not just Ractors. But I do get that it'd be a footgun for many.
- kubectl_h 2y agoI agree. I've thought about POCing some ractor usage for actual CPU bound workloads but ultimately it would be an academic exercise and most of our engineers aren't even thinking about the code performance on that level. My beef, if you will, with async (and I realize now I was a little loose in making the distinction between async and fibers) is that engineers reach for async because they are intimidated by threaded code. Instead they are used to the ergonomics of async/await in the browser and think it's basically the same with the async library in ruby and is somehow immune to the considerations you have to make when spawning a thread. This just isn't true. The dragons are mostly the same, or at least they are in our code base.
- jayd16 2y agoEver done any UI programming or other paradigms where you have a specific OS thread used for cross runtime synchronization but you need to efficiently call background threads knowing exactly what's running where? Async/await handles that well and so far the other paradigms just have not targeted these use cases well. Message passing and promise libraries end up looking a lot like async/await with less sugar. (Compared to C#'s async/await implementation. The level of sugar depends on the language, of course.)
- binary132 2y agoIn our C++ stack at work we accept HTTP requests, and then spawn internal IO which takes some time. During that time, we yield the handler thread to handle other requests. The arrival of the internal response sets up the next step in the request handler to resume, which needs the response message. This can be done manually by polling status of descriptors, and stepping into and out of callables / passed continuations by hand, or it can be done with a task scheduling API and typesafe chained async functions. Pick your poison, I guess, but it probably scales better than using synchronous IO and thread-per-request.
- kmeisthax 2y agoJavaScript has async/await because it solves a very specifically JavaScript problem: the language has no threads and all your code lives in the main event loop of the browser tab. If you do the normal thing of pausing your JavaScript until an I/O operation returns, the browser hangs. So you need to write everything with callbacks (continuation passing style), which is a pain in the ass and breaks exception handling. Promises were introduced to fix exceptions and async/await introduced to sugar over CPS. There's also a very specifically Not JavaScript problem (that happens to also show up in Node.js): exotic concurrency scenarios involving lots of high-latency I/O (read: network sockets). OS kernel/CRT[0] threads are a lot more performant than they used to be but they still require bookkeeping that an application might not need and usually allocate a default stack size that's way too high. There's no language I know of where you can ask for the maximum stack size of a function[1] and then use that to allocate a stack. But if you structure your program as callbacks then you can share one stack allocation across multiple threads and heap-allocate everything that needs to live between yield points. You can do exotic concurrency without exposing async to the language. For example, Go uses (or at least one point it did) its own thread management and variable-size stacks. This is more invasive to a program than adopting precise garbage collection and requires deep language support[3]. Yes, even moreso than async/await, which just requires the compiler transform imperative code into a state machine on an opt-in basis. You'd never, ever see, say, Rust implement transparent green threading[2]. To be clear, all UI code has to live in an event loop. Any code that lives in that event loop has to be async or the event loop stops servicing user input. But async code doesn't infect UI code in any other language because you can use threads and blocking executors[4] as boundaries between the sync and async worlds. The threading story in JavaScript doesn't exist, it has workers instead. And while workers use threads internally, the structured clone that is done to pass data between workers makes workers a lot more like processes than threads in terms of developer experience. [0] Critical Runtime Theory: every system call is political [1] ActionScript 3 had facilities for declaring a stack size limit but I don't think you could get that from the runtime. async/await syntax in Rust also just so happens to generate an anonymous type whose size is the maximum memory usage of the function across yield points, since Rust promises also store the state of the async function when it's yielded. Yes, this also means recursive yields are forbidden, at least not without strategic use of `Box`. And it also does not cover sync stack usage while a function is being polled. [2] I say this knowing full well that alpha Rust had transparent green threading. [3] No, setjmp/longjmp does not count. [4] i.e. when calling async promise code from sync blocking functions, just sleep the current thread or poll/spinwait until the promise resolves
- didip 2y agoAsync/await is just a sugar that many people happened to like. As for me, I like Go's CSP better.
- Fire-Dragon-DoL 2y agoI'm with you here, actor model is the way to go. I thought Go style could be better, but the Go type system is completely inadequate to support that style: it is impossible to guaranteed that a goroutine is panic free, it is not possible to put a recover in the goroutine unless that code is under author's control (could be a lib) and a goroutine panic takes down the whole app. Suddenly I want a wrapper around each goroutine that bubbles up the panic to the main thread without crashing the app, that sounds a lot like an erlang supervision tree
- explaininjs 2y agoThat sounds a lot like async/await. Any errors thrown in an async context bubble to the callsite awaiting it.
- Fire-Dragon-DoL 2y agoSorry you are right, that was specific to our case where we were already waiting for a result from the goroutines. In an actor model, you would let the "goroutine" die and the supervisor would restart it if it has a restart policy, otherwise it will die and stay dead (without bringing the system down). In erlang you can also "link" the current actor to another actor so that if the linked actor dies, the linker dies too (or receives a message)
- hosh 2y agoI have written Ruby, Elixir, and Typescript on Nodejs. I have yet to see a good reason for async/await, other than syntax sugar on a flawed language architecture. The very thing people like about Nodejs (async reactor), creates a lot of problems in production web and data pipeline code. As an aside, Elixir’s Task.async and Task.await are function helpers that work with message passing primitives. Code execution can be truly suspended, and messages are queued. Javascript’s async/await queues code execution rather than messages, and I think that leads to error prone code by design.
- raggi 2y agoasync/await like many similar constructs buys you the ability to defer design decisions you would otherwise have to have made earlier. in a program that stays small, this is good, less work to do. in a program that grows large, this is a hazard and will likely lead to substantial debts down the line.