A Design Space Exploration of Async/Await
Posted by wcrichton 3 days ago
Comments
Comment by biorach 17 hours ago
I think async is deceptive in that it seems like a self-contained and relatively straightforward aspect of a language. But there are many design choices to be made and they all have wide implications.
Plus I think the implications of many of these dimensions are not fully understood and that collectively we are still trying to understand how they are playing out in implementations. Add to this the subtle nature of some of the implications plus the combinations...
I think a good comparison is lexical vs dynamic scope in programming languages. This is a design dimension that was argued over for a decade or two in the early years of programming language design. It was only as time went by, and experience gained by working with concrete implementations that it became clear that lexical scoping should be the default choice and dynamic scoping should be restricted to various niches.
Comment by mitxela 35 minutes ago
Comment by cpa 8 hours ago
I had a course at uni where we dissected how different languages approached concurrency, parallelism, modules/OOP, metaprogramming, eager vs lazy evaluation, types, exceptions... Understanding the trade-offs each language made (and their historical lineage) taught me much more about programming than any Python/Java/C course and made it much easier to pick up new languages.
Comment by weinzierl 4 hours ago
Comment by faresahmed 3 hours ago
Comment by galaxyLogic 9 hours ago
What makes it worse is that you can not simply modify a sync-function to become an async-function, if it has existing callers because those would likely break them.
This affects the whole tree of possible function calls in my program. If at some level I have a sync function but I see it needs to get to some data that only async fuction can provide, I may need to change a whole call-chain of my call-tree, not just modify a single function in that tree.
Then as I develop my program I need to make the decision for every function; should it be sync or async? In many cases it could be either one so which should I choose? Making it async would seem to make it easier to evolve the program later. But then would it make sense to make every function async?
Comment by brabel 6 hours ago
In summary, async is something that looks problematic in theory, but in practice it just works really well!
Comment by valcron1000 3 hours ago
I recommend reading https://degoes.net/articles/no-effect-tracking . In summary, most languages could do with the Go/Java virtual thread async model dropping async/await entirely.
Comment by jeremyjh 3 hours ago
Comment by hombre_fatal 4 minutes ago
Comment by brabel 2 hours ago
Comment by mrsmrtss 6 hours ago
Comment by e1g 8 hours ago
[1] https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
Comment by josephg 8 hours ago
I usually arrange my programs to have a call tree of all my async code, and separate call trees of sync processing work. You want to know ahead of time which is which. If you have a sync function which needs data that’s only available via a network request, take that data in as a function parameter or something. And make the caller responsible for making that data available before the function is called.
It’s a simple model. It’s fast and quite easy to understand once you’re used to it. But you do need to plan ahead, and structure your programs with a plan.
Comment by e1g 7 hours ago
For example, say you have a workflow where, when someone signs up, you generate a user label, e.g., `$firstName $lastName`. You decide to move that to a function that might consider their personal title, preferred name, etc. Currently, it's a pure sync operation. Then, you discover people don't fill out that form at all, but some log in with Google, and you can use the name there as a fallback. Under flexible, this change is local: you can put that into your `createUserLabel(userInfo)`, and it can decide to fire off an API call to fill in any missing data, etc. In Promises land, this would taint the entire tree of everything everywhere that called that method, and all of those things must evolve or be refactored. In an Effectful system, this (previously unplanned) change remains isolated to that one function. Multiply that by every decision over multiple years for software looking for PMF, and requiring developers to "know ahead" severely slows down your ability to evolve.
Comment by mitxela 33 minutes ago
This works for all applications, but not libraries where you don't control your callers. In that case it may make sense to make something async pre-emptively if you think requirements might change in a way that requires it but you can never predict every change successfully and you might need to make a V2 library.
Comment by mrkeen 1 hour ago
Comment by josephg 4 hours ago
I’d probably insist on doing that even in a blocking language where it’s not necessary. Interspersing database or network requests all through a codebase is horrible. Before you know it, someone is calling that function in a loop and you’re doing N serialised database queries. And you can’t even tell that that’s happening from the function signature. Your program just gets slow as your database grows. To say nothing of the correctness problems from issuing these queries outside of a transaction.
I worked on a project that was written like this in Python. The code was packed full of “convenient” sql queries. Some http requests took seconds to render. Turns out those request handlers were issuing thousands of individual sql queries, loading hundreds of megabytes from our database. A lot of the queries were redundant. The backend was just overfetching the same data over and over in tiny helper functions. Because of how the code was written, fixing performance required huge refactors all over the codebase.
File, network and database queries should not be spread all over “for convenience”. Fetching user data and processing it are different tasks. They generally shouldn’t be combined into a single function.
Comment by simonask 4 hours ago
Effects are just a generalization, where async/await is one particular effect.
But: The fact that an operation now does some kind of I/O, or waits for user input, or whatever else you might express using async, has an _enormous_ impact on the architecture of your program. The “virality” of async is completely a feature, because it forces you to actually deal with that change, resulting in much more robust software.
It’s “inconvenient” because the architecture of your program changed. That’s what the job is, though. Languages that don’t help you here (by hiding that you made a change with huge ramifications) make it actively harder to deliver working software, in my opinion. You get there faster, but it won’t keep working.
Comment by mitxela 26 minutes ago
You can have the compiler automatically recompile map with async to make map<async>, likewise map<pure> and map<nofail> and map<noblock> but they will not be optimal; map<async> could be parallel but isn't. And you probably want to control the amount of parallelism at each call site, which just makes it a completely different function. It's likely that you wrote map in a way that uses a loop counter and it's possible the compiler can't prove it's pure. map<abortable> is likely correct, but the compiler has absolutely no way to prove that, and other functions won't be correct if you naively make them possible to abort from outside.
Comment by mitxela 24 minutes ago
Comment by fpoling 3 hours ago
Comment by RossBencina 7 hours ago
Comment by josephg 4 hours ago
Comment by whilenot-dev 6 hours ago
No, that doesn't make sense at all! You're being too reductionist...
I/O has its place in every real world program, and the true limitation (or what you call a "problem") are the single-threaded runtimes of JavaScript. It's not a question of whether you should mark your functions async or not, as if it's an issue of consistency in the call-tree of your program. The true question should rather be whether your functions are I/O-bound (and would actually block the single-threaded event loop) or are solely compute-bound.
You're forgetting the fact that async/await in JavaScript was a historical design choice to prevent the callback-hell that came with single-threaded concurrency. So if you'd want to get back to that callback-hell (and convert async functions back to "some-form-of" sync), you still can[0]:
// some dummy async function that doesn't really do any I/O
async function add(
a: number,
b: number,
): Promise<number> {
return a + b;
}
// convert async function back to sync to enjoy callback-hell again
function addUnpromisified(
a: number,
b: number,
cb: ((result: number | null, reason: any) => any),
): void {
add(a, b)
.then((result) => { cb(result, null); })
.catch((reason) => { cb(null, reason); });
}
You can think of async/await as an evolution of generators[1], as every await yields control back to the event loop. I'd actually encourage you to write some of your programs' behavior with generators if you've never done that before. Generator functions will yield control back to the caller, which is an interesting way to design programs when the caller is you instead of the event loop.It's in Python, but David Beazley still has one of the best explanations on that topic, and shows the how and why you'd want to design an event loop live on stage[2].
[0]: https://www.typescriptlang.org/play/?#code/PTAEGcHsFsFNQCYFd...
[1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guid...
Comment by spankalee 16 hours ago
I'm building a new language with async/await and had to make a lot of these decisions, but I didn't have this organized of a framework to ground myself in. I'm happy to see it clearly that I choose mostly Trio with a bit of JavaScript.
My language (Zena's) async docs page: https://zena-lang.dev/guide/async/ I think I might do a pass and try to call out the decision points more explicitly.
fwiw, I found this post on cancellation by the author of Trio to be vey compelling: https://vorpus.org/blog/timeouts-and-cancellation-for-humans... and I based the cancellation design of Zena on it.
Edit to add: I do wish this included JavaScript's AbortSignal in the Cancellation section. Not because it's good, but because passing cancel tokens is a pattern that exists. There's also the dimension of who can cancel and, like AbortSignal, whether tasks have to opt-in to cancellation checks.
Comment by bufordsharkley 15 hours ago
Comment by jeremyjh 3 hours ago
I realized I have very little experience with async/await; I've only used it extensively in Javascript and only in the browser there - so if my understanding of the exercise hinged on semantics of child_process.spawn then I had no reference point at all for that.
The languages that I have used extensively for back-end work either have native green-threads (Elixir, Haskell), or further back in my career I simply used synchronous I/O in Java and C# which only offered async or futures long after I'd moved on from them.
Frankly, this is a big part of why I chose Elixir and Haskell (and lately, some Go).
edit: Also thanks for your work on Zena, and mentioning it here! I've looked for exactly this before. Now I just have to invent a project for it :)
Comment by theamk 12 hours ago
For example, Trio has no global "spawn" method, by design. Judging by the results, authors assumed "with trio.open_nursery() as n: n.start_soon(write_to_log())", and so they got eager execution, dynamic extent, destructive propagation.
But opening a nursery just to write a single log line is absolutely crazy! The real program would use an appropriately scoped shared nursery: either per-request or global. Later option allows indefinite extent and "never" propagation.
Also, that "()" after write_to_log matters! If one follow trio's own examples, you'd write "n.start_soon(write_to_log)" - note no (). This will switch to lazy execution.
I am not familiar with non-python frameworks listed, but I would not be surprised if they allow for similarly wide range of behaviors.
Comment by crabbone 32 minutes ago
Also, a lot of discrepancy between results is explained by how long the program waits for spawned but unawaited tasks before exiting. In a realistic program, this situation would be considered a bug (spawning a task w/o awaiting it, and then missing the results because the program exits too soon). I can't imagine a situation where the program's author would intentionally create a situation where non-deterministically, a part of the program might not run...
Comment by wzdd 12 hours ago
Comment by jcelerier 16 hours ago
> We cannot attribute C++ to any particular design point in the taxonomy provided in Table 1 because each axis is configurable. Although elegant and neutral, the choice of full programmability makes each library an async dsl; knowledge transfer between projects within the same language becomes exceedingly difficult.
It is not if you think in terms of these axes and which solve your particular problem and not any particular specific design. Take for instance the simplest program one can imagine: a network video player. E.g. some server sends you RTP audio & video frames and you have to play them back correctly, with a nice GUI on top. If you want to do this in a way that is as efficient as possible you need to be aware of all possible ways of async interoperation:
- connecting & receiving packets from the network in a classic network state machine where coroutines shine
- handling vsync vs not-vsync for displaying the video frame
- conforming to whatever async paradigm the hardware video decoding system you want to use is going to provide you with, e.g. Intel QuickSync vs VideoToolbox vs NVDEC...
- handling the synchronous model of audio playback driven in pull mode
- handling the synchronisation between audio / video, and thus the async patterns that support multi-threading as your audio thread can't be your video or GUI thread
- handling the async model of your GUI library for your play / stop button's callbacks.
There's zero chance that a single async model fits all of these equally well without tradeoffs, so you have to have the knowledge anyways.
Comment by bombela 14 hours ago
Decoding video/audio and talking to the right OS APIs and GPU is far from simple. It is reasonable to implement a http1 client from scratch by hand. For decoding, you need libraries/dependencies. And suddenly you have to find the intersection of dependencies that play nice in your async model of choice.
Comment by flossly 4 hours ago
I just want to keep it simple.
Async "infects" you code: for it to bring benefits your whole codebase needs to be doing it (ingesting requests, db calls, web API calls).
Due to this we see "split" stacks in programming languages: one lib stack for synchronous, and one for async.
I did not think the benefits of better performance under load is worth the mental overhead of doing async everywhere. So i went with blocking calls and virtual threads. No regrets.
Comment by tcfhgj 4 hours ago
not really - the core of apps (usually no outside dependencies), and additions which solely rely onthe core, usually can be implemented without async entirely; async only comes into play once you add dependencies to file system, network and ui, but you don't need to make the core async for that. You might not call some functions in async code at all, because the function is used only for heavy computation which is best handled by dedicated threads to avoid stalling your io handling.
Comment by vips7L 1 hour ago
Comment by gugagore 4 hours ago
Simply-typed lambda calculus guarantees that the computation terminates. Sometimes you need non-termination. Fixed point operators is one thing that brings in all the stuff that you threw out.
Linear logic is good for the bits and pieces of concurrency where you don't need concurrency. Linear logic guarantees that there are no race conditions. Sometimes you need race conditions — how do you fit that in there? Sometimes, having race conditions is really important: I am selling tickets and there is going to be a race for who gets the last ticket. Is there a single thing you can add to linear logic that would give me race conditions? Not known. - Philip Wadler on Type Theory Forall #54 - The Goal of Science is to Communicate Ideas!
Comment by biorach 18 hours ago
Comment by xboxnolifes 42 minutes ago
Comment by hankbond 14 hours ago
and i took that personally
Comment by brabel 6 hours ago
Comment by jquery 12 hours ago
Comment by bradleybuda 18 hours ago
Some of these design decisions seem indefensible to me. For example, what the authors call "Suspension":
-> Static: Await points guaranteed to suspend -- JavaScript
-> Dynamic: No guarantees on awaiting tasks -- C# · Swift · Tokio · Smol · Asyncio · Trio
What is "await" if not a synonym for "suspend"?!?
async/await is one product of a long line of thought that says "threads are too hard for programmers to get right". Threads (really, shared memory) have real usability issues for developers, but once you grok the semantics (which largely map to the physical execution model in a CPU) that knowledge is transferrable across virtually all languages and runtimes.
Comment by toast0 17 hours ago
Having used both threads and async/await and using them both in the same program, I don't see how async/await is supposed to make it easier to get right.
In my experience, async/await seems to be a solution to avoid running too many threads. In Javascript, because you could only have one thread in browsers; in other languages because thread per X is too many threads and queuing to a thread pool might not be desirable either.
Async/await always feels terrible to use though. Some other way to get thread like semantics without having to have OS threads for everything seems better (to me). Erlang processes, Java Loom Virtual Threads (which I haven't used), etc. If it avoids having all memory shared, even better.
Comment by jerf 17 hours ago
The problem is, the community collectively decided the problem was "threading" in general rather than "trying to have tons of threads running around shared data structures controlled via piles of simultaneously-held semaphores" specifically.
If you don't structure your threads on that basis, but instead default to something that looks more like actors and message passing, even if it isn't strictly speaking actors and message passing, the complexity comes down. Add some later elaborations like structured concurrency and a few other pre-canned design patterns for threading like a parallel map or worker pools being issued work items and it becomes merely something difficult rather than insane. When you program with threads sanely, it takes very little for async/await to actually be the substantially more complicated and difficult-to-understand choice when you have a workflow more interesting than "always await everything immediately" to implement, to say nothing of how nice it is to have things actually running on multiple cores simultaneously without having to carefully arrange for it.
Comment by theamk 12 hours ago
Also, I think that single-threaded programs, even with co-routines, are just so much nicer than multi-threaded ones. You write "index = last_index++;" and it Just Works (tm), no need to worry about locks or atomics or other thread access.
Comment by mitxela 9 hours ago
Comment by jerf 2 hours ago
More people think they need the latest and hottest in manual memory management than actually do.
More people think they need a hundred thousand threads than actually do.
If you do have one of those cases, by all means prepare for it and deal with it. But be sure you have one first. The program that exceeds so much as a 100 threads is not only exceptional, but very exceptional. The exceptions are cognitively available and leap to mind, but are nevertheless the exceptions. And, again, if you have one, deal with it, but be sure you have one.
If you're sitting there in TypeScript land writing "async" code you've already surrendered on Ultimate Efficiency anyhow. Deciding what is more efficient between a threaded program in a runtime that doesn't box everything and JIT-optimized JS code is difficult but it isn't that hard for the threaded program that isn't boxing to win out on all runtime measurements, including consumed RAM.
"You write "index = last_index++;" and it Just Works (tm), no need to worry about locks or atomics or other thread access."
That goes back to my comment about using better threading techniques. I write a lot of "index = last_index + 1" (mutably incrementing like that in a single expression is just bad style anywhere you see it) in my threaded code all the time without thinking much about it, because I use the model where by default a value belongs to the one actor process that has access to it at all. The problem isn't that mutation is dangerous in threaded code, the problem was people writing threaded code based on a ton of threads running around shared data structures with locking. Not only is that not the only way to write threaded code, it is literally the worst. There are many other options, all of them better in some way, many of them much better.
Contrasting the difficulty of writing threaded code to something else based on the assumption that "lots of shared state locked by semaphores" is the only way to write code is like a Haskell advocate talking about the amazing benefits of functional programming while writing as if literally every imperative program is just one big pile of unmitigated, pure spaghetti code where everything is linked together with gotos and every variable in the program is a global variable. That's not the relevant comparison any more. It hasn't been for a long time. If anyone's program is scrambled because they did write a big pile of gotos and global variables or they did write a big pile of shared state with semaphores everywhere, that's on them. A vast array of better techniques of all shapes and sizes was available to them.
Comment by PhilipRoman 8 hours ago
Comment by e4m2 7 hours ago
https://learn.microsoft.com/en-us/cpp/build/reference/stack-...
Comment by spinningslate 7 hours ago
That's true but I'm puzzled by the decision rationale. It's undeniably a major undertaking to add first class, fine-grained processes to a language and its runtime. But time invested there gets the multiplicative upside that all language users benefit from the investment. Instead, Async/Await transfers the complexity to users of the language, as TFA describes.
As an Erlang and now gleam developer, I'm continuously grateful for the BEAM's support for fine-grained processes (note these are VM processes, not OS level). If I want to do things in parallel, I spawn a new process to do it. Do I want that concurrency because of io latency or parallel computation? Doesn't matter. Processes handle both. If I want an actor - a long(ish) lived "object" that responds to messages sent to it - I spawn it as a process. If I want to communicate between processes, I send a message. That's the only choice. No shared memory so no semaphores, locks and whatnot.
I never have to think "hmm, should this function be sync or async?" and reason about the transitive implications through the entire call stack. I write functions to calculate values. If I want function A to be called after function B in program 1, I write them sequentially. If I want to run them concurrently in program 2, I spawn them in separate processes. Concurrency is a decision at the calling site, not when writing the function being called.
One concurrency primitive that meets all the needs. The reduction in cognitive load is palpable compared to Python (the other language I use regularly).
The usual reaction is "yeah but performance". I've never found this to be an issue in real life. Sure there are benchmarks that show C/Rust/C#/whatever is faster, often meaningfully so. In practice, for my needs: never been a problem.
I'm ever more grateful for the elegance and consistency of the BEAM concurrency model. From an ergonomic perspective, Async/Await feels like a poor abstraction by comparison.
That's not to say the BEAM (or its languages) is the final word in concurrency. The strong encapsulation boundaries from Structured Concurrency[0] would be a useful addition. Though even there, Erlang's supervisor hierarchies provide a a similar mechanism. Dataflow is another interesting area (many task-concurrent design questions are essentially dataflow problems).
Even without improvement though I'd still take Erlang's approach over Async/Await every day.
Comment by rerdavies 16 hours ago
Comment by switchbak 14 hours ago
I do find that the ergonomics of this are highly dependent on a few features of a language runtime, without which it all falls apart. Or you need language specific syntax and typically a single standard implementation.
Comment by rerdavies 10 hours ago
And I can't honestly think of another paradigm that doesn't require callback functions or lambdas that, ergonomically, end up producing function implementations that end up drifting off the right side of the screen for anything more than a couple of sequential asynchronous operations.
Comment by switchbak 7 minutes ago
“I can't honestly think of another paradigm that doesn't require callback functions” … Haskell, Scala, Rust all use various approaches to asynchrony that leverage these language features to provide you very usable abstractions without the “callbacks” you mention. Some of those lean on lambdas, but the scrolling off the right issue hasn’t been an issue there for at least a decade now.
Scala’s direct mode is interesting, as an example of library driven, blocking/imperative style interactions that provide most of the benefits of the monadic effect style, but in a way that’s far easier for a human to write and review.
This might not be ready for mass adoption yet, but I think it’s a sneak peek of where we’ll see some languages move to.
Comment by marcosdumay 12 hours ago
Comment by theamk 12 hours ago
Comment by marcosdumay 3 hours ago
At most you get hidden behavior on exception propagation and end-of-life extent.
Comment by jerf 2 hours ago
If I sat down and made a careful study of all the threading implementations we might get up to a similar number of quirks.
I would suggest though that the dimensions are generally more likely to be corner cases. Some of the dimensions mentioned in that article are fairly in-your-face for an async/await implementation and can cause serious difficulties migrating between systems fairly quickly if you make the wrong assumptions, and writing correct async/await code that isn't just straightline "await everything immediately" code has to start taking some of those things into account very quickly. The equivalent for threading is more likely to only come up rarely and in more cases the correct answer is really "don't depend on that anyhow", e.g., rather than depending on exact details of how a thread is terminated to accomplish something, just cleanly send a message with your results to whoever it is waiting for it directly and let the runtime do the cleanup without your code witnessing any effects of it. Depending on these quirks in threading code is much more likely to be bad engineering practice, rather than necessary engineering practice in the async/await case.
Comment by yxhuvud 8 hours ago
Comment by AdieuToLogic 14 hours ago
The `await` keyword in most languages is not a synonym for suspending thread execution so much as it is an effectual attempt to replicate the functionality of `coreturn`[0]. To wit, if an underlying `Future`/`Promise` has completed before the `await` instruction is evaluated, the thread executing same will not be suspended.
0 - https://www.euclideanspace.com/maths/discrete/category/highe...
Comment by nxc18 18 hours ago
There are scenarios where something might need to await and might not. Why take the hit if you are able to do something synchronously? Edit: this is especially important given the “viral” nature of colored functions.
It does make it hard to reason about, but this kind of problem is all over the place - e.g. very similar-looking code can have very different semantics depending on your framework if you’re using jsx or a particular decorator means one thing in one project and something else in another. That’s just part of the game at this point.
Comment by bmm6o 18 hours ago
I don't really understand gp's point. From inside the code, you can't tell if there was a pause or not. Clock time or thread id are heuristics, but you can't really be sure.
Comment by nottorp 6 hours ago
Also message loops and state machines :)
Comment by biorach 18 hours ago
it's a question of whether the runtime is guaranteed to suspend at an await point or if it may choose not to
> async/await is one product of a long line of thought that says "threads are too hard for programmers to get right".
what? no! concurrency vs parallelism etc etc
Comment by danilocesar 18 hours ago
Comment by pansa2 11 hours ago
Apologies if it does and I’ve glanced over it - but if it doesn’t, is there another resource that compares stackful coroutines to stackless in a similar way?
Comment by mitxela 9 hours ago
Comment by homarp 2 days ago
Comment by _ink_ 10 hours ago
Comment by rawling 9 hours ago
C: if nothing is waiting for it, don't run it at all.
ABC: if nothing is waiting for it, wait for it when it's run.
Comment by _ink_ 1 hour ago
Comment by weinzierl 4 hours ago
Comment by crabbone 43 minutes ago
What creates the implementation difference is the side effect. Some runtimes may, legitimately, conclude that since the task hasn't been awaited, then it shouldn't run at all, and no side effects should happen. Other runtimes either lack this kind of sophistication, or believe that the side effect is the goal of spawning the task, and so they proceed to run it anyways.
Other discrepancies between runtimes are explained by the non-deterministic nature of concurrency... They happen to be more predictable in a very simple program that happens to terminate before the unawaited task has a chance to complete, which is what creates such diverse answers. I imagine that if the program waited longer, then we'd see most if not all implementations print all of the A, B, and C, where C can be first, second or third, but B must follow A. Which is what you'd expect, if you are familiar with any async framework.
Comment by dmix 12 hours ago
Comment by rao-v 14 hours ago
It’s why I feel go (with go routines being the norm) is one of the few imperative languages that was designed vs. filling out a bunch of historical constraints (apologies this is not meant to trigger a language debate, just an idiosyncratic thought)
Comment by kccqzy 14 hours ago
And of course go routines and channels can also be desugared into mere control flow. That’s how ClojureScript does async.
Comment by aw1621107 14 hours ago
Do you mind elaborating on this? I don't understand what you're trying to get at.
Comment by rao-v 12 hours ago
Go sort of asks why tho and just standardizes on go routines as a good abstraction over both concurrency and parallelism.
This is sorta true elsewhere too. Go rejects a lot of the machinery that OO languages seem to feel obliged to carry around - inheritance hierarchies, explicit interface implementation etc. For what it's worth, I don't write much go, and I don't think it's magical. I just like how clearly it revisited some basics.
Elsewhere on HA you’ll find my extended rant about how strange it is that we don't have a language that elegantly abstracts computation over threads, SIMD, GPUs etc. Compilers can do this sort of thing now, just not optimally.
Comment by jcranmer 11 hours ago
Autoparallelization has been a hot topic for literally decades, quite possibly longer than you've been alive.
The problem is that the techniques you need to do to write good SIMD code versus good GPU code versus good multithreaded code versus distributed computation are all different. Taking just memory concerns: a SIMD code needs you to carefully arrange memory so that every thread is accessing an adjacent memory location. GPU code likes locality, but you have large group sizes that can share all the local memory pretty cheaply, and loading from global memory to local memory is relatively expensive, so now you have to do a lot of tuned blocking. With multithreaded code, you now want to avoid sharing between different threads (which generally requires distributing loop iterations among threads very differently). And with a distributed platform, now you're primarily worrying about the overhead of communication of data between different nodes, and you're trying to minimize that.
Comment by mitxela 9 hours ago
Comment by rao-v 10 hours ago
The point (and I'd encourage you to find that thread to not retread ground) is that we absolutely can compile most computation heavy code for these different targets reasonably well - what we cannot garentee is that the resulting code is optimal given context. But gosh we can do so much - I’d encourage you to look into in profile guided, target aware, and autotuning optimization etc. (and then of course, there are LLM guided optimizations, but that's a whole other kettle of fish)
Comment by tcfhgj 9 hours ago
if it is really that good, why didn't Rust adopt the same thing?
Comment by bobnamob 8 hours ago
Comment by ksh09 6 hours ago
Comment by alilleybrinker 18 hours ago
Also a great teaching tool, if someone knows one async system, to be able to show them the differences on each axis from their prior one to a new one they’re learning.
Comment by perrygeo 19 hours ago
Comment by glaslong 17 hours ago
Feel like I should assign myself a couple dozen Jon Skeet posts to read now, to make up for this embarrassment.
Comment by jameshart 15 hours ago
Comment by layer8 19 hours ago
Comment by strideashort 6 hours ago
it could be sth along the lines of:
on(x=foo()){ //land here when x is computed } catch{ //sth got wrong with foo }
Visual basic was superior to async/await crap. Not even joking.
Comment by vitaminCPP 17 hours ago
Comment by ameliaquining 15 hours ago
The 0.16 release earlier this year introduced a much-heralded userland API (https://ziglang.org/documentation/master/std/#std.Io) that can be used to implement various asynchrony and concurrency patterns, including green threads, but it can't do stackless coroutines because support for those has to be baked into the compiler.
There is currently an open proposal to bring back stackless coroutines without dedicated syntax (instead offering low-level bring-your-own-buffer APIs for interacting with suspended coroutines), which could be combined with the aforementioned userland API to produce something more like how async/await works in other languages (https://github.com/ziglang/zig/issues/23446).
Comment by moralestapia 19 hours ago
Comment by worik 11 hours ago
After all these years doing cooperative multitasking again
Comment by cbm-vic-20 18 hours ago
Comment by yxhuvud 8 hours ago
Comment by mitxela 9 hours ago
Both efforts, instead of trying to avoid threads because they are expensive, simply asked why they have to be expensive and then made them not expensive.
Comment by tcfhgj 8 hours ago
Comment by mitxela 7 hours ago
Comment by tcfhgj 5 hours ago
Comment by biorach 17 hours ago
Comment by MichaelNolan 16 hours ago
Comment by PhilipRoman 8 hours ago
Comment by slopinthebag 17 hours ago
Comment by jdw64 18 hours ago
I think that's definitely right. Knowing the semantics of the language you mainly use is important.
Comment by agumonkey 18 hours ago