cpa 3 hours ago

This kind of semantics-first comparative analysis of programming languages is so important.

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.

biorach 12 hours ago

It's long been clear that there were fundamental implementation choices that mattered between async runtimes, but _nine_ design dimensions? Damn.

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.

galaxyLogic 4 hours ago

The problem I encounter with async/await (in JS) is that while an async method can call a non-async-function and do somewthing with the result of that, the reverse is not true, a sync function can call async-function but can not us the result of that in any way, except pass it on or upwards.

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?

  • e1g 3 hours ago

    Tactically, this problem is commonly known as “colored functions”[1], and the only option in JS is to have some other runtime coordinate your function execution; in JS, that solution is Effect[2]

    [1] https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...

    [2] https://effect.website/

    • josephg 3 hours ago

      There are a lot of libraries which can help you deal with this, but ultimately the parent is right.

      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.

      • RossBencina 2 hours ago

        I'm curious about your mental model. Would it be accurate to say that the async tree is the "IO program" and the sync functions operate on pure data, or is it more complicated than that?

        • josephg 15 minutes ago

          Yeah, more or less.

      • e1g 2 hours ago

        A hallmark of good architecture is adaptability to unexpected changes in requirements. Planning ahead helps with 'known unknowns', but it's impractical when building across N years in a dynamic environment - "knowing ahead of time" is just not possible for anything non-trivial. You need strong architectural primitives that don't scale based on developers' omniscience.

        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.

  • brabel 2 hours ago

    This is often brought up as if it were a problem, but I see async functions as something similar to IO in Haskell. Almost all asynchronous functions I write are asynchronous because they will do IO of some sort. Async functions end up being markers of where IO may occur, which is very useful. It is very rare that I need to change a function from being sync to async (and the inverse pretty much never happens), and when that happens it's usually not a big deal (the caller is highly likely to be an async function within a short stack distance, so only one or two functions in the middle normally need to change).

    In summary, async is something that looks problematic in theory, but in practice it just works really well!

    • mrsmrtss 1 hour ago

      Agreed on async. You better know if a function does IO, hiding that can lead to nasty surprises.

  • whilenot-dev 1 hour ago

    > But then would it make sense to make every function async?

    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...

    [2]: https://www.youtube.com/watch?v=MCs5OvhV9S4

spankalee 11 hours ago

Wow, this is really helpful and timely!

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.

  • bufordsharkley 10 hours ago

    I definitely find trio (formerly curio) to be so thoughtfully designed at every turn; it's dispiriting that it never seemed to gain much of a user share over asyncio (whose main advantage appears to simply be inertia and stdlib privilege)

theamk 8 hours ago

Great post, but the quiz is unfair - it assumes there is only one "true way", but a lot of frameworks give you options

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.

  • wzdd 7 hours ago

    Agreed. I was confused by the Trio example until I reverse engineered what they meant from the outputs. Trio behaves differently (in well-defined, easy to understand ways) depending on where you put nurseries.

jcelerier 11 hours ago

I was wondering "hopefully C++ allows you to pick across these axes so that you can build yourself the async primitives that work best for the problem at hand" and then: yes!

> 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.

  • bombela 9 hours ago

    Agree with your post except the adjective "simple" for a network video player.

    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.

biorach 13 hours ago

At last someone took the time to pore over all the tedious crap that I have been trying and failing to keep straight in my head since forever.

hankbond 9 hours ago

> You must be a JavaScript developer.

and i took that personally

  • jquery 8 hours ago

    I was upset and felt like I did something wrong.

  • brabel 1 hour ago

    Got that too... but on JS's defence, I think the JS behavior is the most "natural" unless you've been trained on the other approaches (where explicitly awaiting is required for anything to actually happen). Dart and Kotlin, for example, also do that (and are not mentioned in the article - would have felt nicer to be told I must be a Kotlin/Dart developer).

bradleybuda 13 hours ago

I answered the quiz and it said "you must be a Javascript developer", which is true enough - that's probably my second-most-proficient language. In fact, I'm a Ruby developer partially because I hate the idea of async/await and I'm feeling very smug about my choice after reading this.

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.

  • biorach 13 hours ago

    > What is "await" if not a synonym for "suspend"?!?

    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

  • danilocesar 13 hours ago

    I'm teaching async calls in makearcade to my 10yo son, to bypass a platform bug. He said he doesn't get it. My answer was: Don't worry, adults don't get it either.

  • nxc18 13 hours ago

    > What is "await" if not a synonym for "suspend"?!?

    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.

    • bmm6o 13 hours ago

      C# has Task.FromResult(), which is useful if you are implementing an interface that allows async work but your implementation doesn't require it. I believe the runtime will check for this case and continue execution. It's better for the cache to keep executing the current task on the current thread.

      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.

  • toast0 13 hours ago

    > async/await is one product of a long line of thought that says "threads are too hard for programmers to get right".

    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.

    • jerf 12 hours ago

      In the 1990s, threads were programmed with extensive use of semaphores and threads arbitrarily running around shared data structures. This is a disastrous approach to threading, and I agree with pretty much every scathing condemnation written about it.

      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.

      • rerdavies 11 hours ago

        The principal difference between dispatching in a thread-based framework and async/await is that async/await allows you to program sequences of asynchronous operations much more easily. No more separation of code that initiates an async operation and the code that handles the result!

        • switchbak 9 hours ago

          These higher level primitives that they mention provide the primitives for exactly that. This can be found in other paradigms besides async/await.

          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.

          • rerdavies 5 hours ago

            Of course. I don't think a language can support async/await without a library implementation, or the language features that support it.

            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.

        • marcosdumay 8 hours ago

          The principal difference between threads and async/await has to be specified in at least 9 dimensions...

          • theamk 8 hours ago

            Very similar dimensions also apply to threads, the async/await is not really that different there.

            • yxhuvud 4 hours ago

              Very much so and it can be argued that the difference between threads and ssync is just another dimension to compare on. For example, essentially everything that is involved in Structured Concurrency is as relevant to parallel scenarios as well.

      • theamk 8 hours ago

        Linux has 8MB thread stacks by default, Windows apparently has 1MB ones, and that space is not going anywhere. As long as people are worried about memory, they will need something lighter than threads. (Yes, golang managed to create dynamic stacks, but this required major support from compiler and so unlikely to appear in existing languages).

        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.

        • mitxela 4 hours ago

          Both windows and Linux let you configure the thread stack size

        • spinningslate 2 hours ago

          > Yes, golang managed to create dynamic stacks, but this required major support from compiler and so unlikely to appear in existing languages

          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.

          [0] https://en.wikipedia.org/wiki/Structured_concurrency

  • AdieuToLogic 9 hours ago

    > What is "await" if not a synonym for "suspend"?!?

    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...

  • nottorp 1 hour ago

    > threads are too hard for programmers to get right

    Also message loops and state machines :)

pansa2 7 hours ago

This looks like a really thorough examination of async-await i.e. stackless coroutines, but it doesn’t seem to cover Lua-like stackful coroutines.

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?

  • mitxela 4 hours ago

    Stackful coroutines are just traditional threads.

ksh09 2 hours ago

Perfect timing, just yesterday I was exploring async implementation and their intricacies in non-GC langs.

dmix 7 hours ago

I learned from publishing a javascript library that people were supposed to put on their own websites then customize, that people don't really understand async/await even if they pretend to know JS and that you should avoid it in baseline documentation. That's changed a bit since this demographic started using LLMs but I'm still a bit wary. I almost don't blame them after using it for nearly a decade.

rao-v 10 hours ago

I remember being so mad years ago, coming from a pure CS background, when it dawned on me that async await was “mere” control flow and not actual parallelism.

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)

  • aw1621107 10 hours ago

    > vs. filling out a bunch of historical constraints

    Do you mind elaborating on this? I don't understand what you're trying to get at.

    • rao-v 7 hours ago

      Threads were historically expensive enough that “just spawn a thread” wasn’t a reasonable thing to do in many situations. Thread pools were sort of a last resort, and we ended up with control flow like objects (futures, await etc.) to multiplex concurrency without parallelism.

      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.

      • jcranmer 6 hours ago

        > 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.

        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.

        • rao-v 5 hours ago

          There is better than even odds I'm older than you, so I'd recommend you rethink using phrases like "possibly longer than you've been alive", it's not ... polite regardless of people's age.

          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)

        • mitxela 4 hours ago

          Another axis: a GPU wants you to load a large batch of work and then start it - you can't be bouncing between CPU and GPU work all the time, but you can mix SIMD and non-SIMD instructions freely.

      • tcfhgj 4 hours ago

        > Go sort of asks why tho and just standardizes on go routines as a good abstraction over both concurrency and parallelism.

        if it is really that good, why didn't Rust adopt the same thing?

        • bobnamob 3 hours ago

          Different design goals, go ships with a runtime baked in, rust wanted an async design independent of runtime implementation that’d be usable in embedded contexts

  • kccqzy 9 hours ago

    Being mere control flow is a good thing: some async/await implementations are just desugared into a state machine anyways, and it totally works on a single thread. Early async/await in Python was just a small generalization of its existing generator mechanism, and nobody would think generators in Python enables parallelism: it was always a control flow construct. This cleanly demonstrates the separation between the concepts of concurrency versus parallelism.

    And of course go routines and channels can also be desugared into mere control flow. That’s how ClojureScript does async.

_ink_ 5 hours ago

Can someone explain what happens in Rust and Python? I don't see how C / ABC can happen (or what's even the point of async when that's the result).

  • rawling 4 hours ago

    I think it's down to them noticing that nothing is waiting for the result of the task and handling it differently?

    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.

strideashort 1 hour ago

Async await is a glorious fucking event loop which obscures the primitive, crude simplicity to something unrecognizable which most developers think does something it absolutely doesn't.

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.

alilleybrinker 14 hours ago

With these dimensions of design variance defined, you could also make a closeness measure in 9-dimensional space and identify the most or least similar combos.

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.

perrygeo 14 hours ago

Amazing work. It's one thing to say "async is complex". It's another to parse that statement so carefully as to have a cross-language theory of async execution. Looking forward to digging into this!

glaslong 13 hours ago

C# is my primary, but the quiz tells me I'm a JS dev.

Feel like I should assign myself a couple dozen Jon Skeet posts to read now, to make up for this embarrassment.

  • jameshart 10 hours ago

    To be fair to yourself, the fact that C# terminates pending tasks when the main method exits is something most C# devs don’t have to deal with because they’re mostly working in the context of long running servers and apps.

layer8 14 hours ago

It would be a fun coding agent benchmark to have them translate such a program between the different languages and see whether they preserve the respective semantics.

vitaminCPP 12 hours ago

Love it. I wish it included zig.

  • ameliaquining 10 hours ago

    Zig doesn't currently have async/await in the sense that this post is about (i.e., async/await based on stackless coroutines). It previously had this, but it was removed last year (https://ziglang.org/download/0.15.1/release-notes.html#async...).

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

worik 7 hours ago

Wierd.

After all these years doing cooperative multitasking again

moralestapia 14 hours ago

Great work. Must read for anyone working with this type of concurrency.

cbm-vic-20 13 hours ago

or, Java Virtual Threads and chill.

  • biorach 13 hours ago

    No. Because concurrency vs parallelism

    • MichaelNolan 11 hours ago

      How does parallelism come into play for this conversation? Async/await is a concurrency construct. And Java’s virtual threads are also a concurrency construct. Neither of them have anything to do with parallelism. Or am I misunderstanding something?

      • PhilipRoman 3 hours ago

        I'd say Java's virtual threads are also a parallelism construct (at least in the performance sense, not logical guarantees), since they're scheduled on a pool.

  • mitxela 4 hours ago

    Also formerly known as goroutines

    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.

    • tcfhgj 3 hours ago

      they are still more expensive than compiled async await, also mixing compute and io-bound work can cause issues

      • mitxela 2 hours ago

        How sure are you? Got a benchmark?

        • tcfhgj 57 minutes ago

          100% sure - Java just has to store and process more than a simple state machine, also if all threads are busy with compute bound work, async work is stalled if you don't put the compute bound work on adedicated thread (pool)

  • yxhuvud 4 hours ago

    Most of the dimensions exist in the threaded world as well, only the dimensions wasn't as explored at that point so the choices are usually not what would have been chosen today.

slopinthebag 12 hours ago

Maybe it’s cuz I started with async/await instead of threads but I cannot relate to people saying it’s harder than threading. To me it’s substantially easier to understand than threads, goroutines, or structured concurrency in Kotlin.

jdw64 13 hours ago

>You must be a C#, Swift, Asyncio, or Tokio developer — hard to narrow down, you all agree on this one.

I think that's definitely right. Knowing the semantics of the language you mainly use is important.