249 pointsby pjmlp5 hours ago19 comments
  • ignoreusernames3 hours ago
    Agree with the other commenters that the title is a bit too dramatic. The content was well written and got the point across.

    I still don’t have enough experience to have a strong opinion on Rust async, but some things did standout.

    On the good side, it’s nice being able to have explicit runtimes. Instead of polluting the whole project to be async, you can do the opposite. Be sync first and use the runtime on IO “edges”. This was a great fit to a project that I’m working on and it seems like a pretty similar strategy to what zig is doing with IO code. This largely solved the function colloring problem in this particular case. Strict separation of IO and CPU bound code was a requirement regardless of the async stuff, so using the explicit IO runtime was natural.

    On the bad side, it seems crazy to me how much the whole ecosystem depends on tokio. It’s almost like Java’s GC was optional, but in practice everyone just used the same third party GC runtime and pulling any library forced you to just use that runtime. This sort of central dependency is simply not healthy.

    • selfhoster13122 hours ago
      What's the alternative? I'm happy to use tokio, but i'm happy other folks can enjoy other executors (smol, async-std, glommio, etc). I think the situation is OK because tokio is well-maintained, even though it's not part of the standard library, and i'm afraid making it part of the standard library would make it harder to use other executors, and harder to port the standard library to other platforms.

      But maybe my fears are unfounded.

      • nicoburns38 minutes ago
        > What's the alternative?

        Traits in the stdlib for common functionality like "spawn" (a task) and things like async timers. Then executors could implement those traits and libraries could be generic over them.

        • josephg3 minutes ago
          Yep. We could have a system like how there's a global system allocator, but you can override it if you want in your app.

          We could have something similar for a global async executor which can be overridden. Or maybe you launch your own executor at startup and register it with std, but after that almost all async spawn calls go through std.

          And std should have a decent default executor, so if you don't care to customise it, everything should just work out of the box.

      • oersted17 minutes ago
        It would make sense to have an official default async runtime in the standard library while keeping the door open to use any other runtime, just like we already have for the heap allocator or reference counting garbage collection.

        There are issues in particular with core traits for IO or Stream being defined in third-party libraries like tokio, futures or its variants. I've seen many cases where libraries have to reexport such types, but they are pinned to the version they have, so you can end up with multiple versions of basic async types in the same codebase that have the same name and are incompatible.

      • ignoreusernamesan hour ago
        As of now I don’t think there’s an alternative. I’m not a Rust expert but the core issue to me is that “async” goes beyond just having a Futures scheduler. Async stuff usually needs network, disk, os interaction, future utilities(spawn) and these are all things the runtime (tokio) provides. It’s pretty hard to be compatible with each other unless the language itself provides those.
        • turdinthemouth15 minutes ago
          That's not the core issue at all it's lifetimes and allocations.
          • ignoreusernames5 minutes ago
            Can you elaborate on this please? Do you mean that’s basically impossible for rust std to provide a default runtime that makes “everyone” (embedded on one end and web on the other) happy?
    • brabelan hour ago
      As you mentioned Java, it’s interesting to notice that it has had similar problems throughout its history: logging (now it’s settled on slf4j but you still find libraries using something else), commons (first Apache Commons, now Guava), JSON (it has settled on Jackson but things like Gson and Simple-json are not uncommon to see), nullability annotations ( first with unofficial distributions of JSR-305 which never became official, then checker framework , and lately with everything migrating to JSpecify). All this basic stuff needs to be provided by the language to avoid this fragmentation and quasi de facto libraries from appearing.
      • jcelerier31 minutes ago
        But this fragmentation is what needed to make good software. If you put things in the standard library you're just adding a +1 to the fragmented landscape because for instance it will never be specialized enough to cover all use cases, so people will still use their own libraries, just like for instance c++ has three dozen distinct implementations of hash maps just because one cannot fit all cases
  • hmry5 hours ago
    Great article! Love these types of deep dives into optimizations. Hope the project goal works out!

    I've felt before that compilers often don't put much effort into optimizing the "trivial" cases.

    Overly dramatic title for the content, though. I would have clicked "Async Rust Optimizations the Compiler Still Misses" too you know

    • diondokter3 hours ago
      So on the title, I picked this because it's simply the truth. Since async landed in 2019 or so, not much has changed.

      Yes, we can have async in traits and closures now. But those are updates to the typesystem, not to the async machinery itself. Wakers are a little bit easier to work with, but that's an update to std/core.

      As I understand it, the people who landed async Rust were quite burnt out and got less active and no one has picked up the torch. (Though there's 1 PR open from some google folk that will optimize how captured variables are laid out in memory, which is really nice to have) Since I and the people I work with are heavy async users, I think it's maybe up to me to do it or at least start it. Free as in puppy I guess.

      So yeah, the title is a little baitey, but I do stand behind it.

      • mplanchard2 hours ago
        Some of the burnout no doubt being due to the catastrophizing of every decision by the community and the extreme rhetoric used across the board.

        Great to see people wanting to get involved with the project, though. That’s the beauty of open source: if it aggravates you, you can fix it.

        • selfhoster13122 hours ago
          As an example of this, i remember a huge debate at the time about `await foo()` vs `foo().await` syntax. The community was really divided on that one, and there was a lot of drama because that's the kind of design decision you can't really walk back from.

          Retrospectively, i think everyone is satisfied with the adopted syntax.

          • zozbot234an hour ago
            It makes sense that there was a huge debate, because the postfix .await keyword was both novel (no other languages had done it that way before) and arguably the right call. Of course, one can argue that the ? operator set a relevant precedent.
          • jurgenburgenan hour ago
            > Retrospectively, i think everyone is satisfied with the adopted syntax.

            Maybe it’s a case of agree and commit, since it can’t really be walked back.

            • diondokteran hour ago
              Various prominent people have said years after that .await was the correct choice after all
      • nixpulvisan hour ago
        I think it's partially accurate, and partially a consequence of how async fractures the design space, so it will always feel like a somewhat separate thing, or at least until we figure out how to make APIs agnostic to async-ness.
        • meowfacean hour ago
          I am a beginner to Rust but I've coded with gevent in Python for many years and later moved to Go. Goroutines and gevent greenlets work seamlessly with synchronous code, with no headache. I know there've been tons of blog posts and such saying they're actually far inferior and riskier but I've really never had any issues with them. I am not sure why more languages don't go with a green thread-like approach.
          • dminik24 minutes ago
            Because they have their own drawbacks. To make them really useful, you need a resizable stack. Something that's a no-go for a runtime-less language like Rust.

            You may also need to setup a large stack frame for each C FFI call.

          • jolux27 minutes ago
            Rust originally came with a green thread library as part of its primary concurrency story but it was removed pre-1.0 because it imposed unacceptable constraints on code that didn’t use it (it’s very much not a zero cost abstraction).

            As an Elixir + Erlang developer I agree it’s a great programming model for many applications, it just wasn’t right for the Rust stdlib.

          • nixpulvis35 minutes ago
            One of Rust's central design goals is to allow zero cost abstractions. Unifying the async model by basically treating all code as being possibly async would make that very challenging, if not impossible. Could be an interesting idea, but not currently tenable.

            One problem I have with systems like gevent is that it can make it much harder to look at some code and figure out what execution model it's going to run with. Early Rust actually did have a N:M threading model as part of its runtime, but it was dropped.

            I think one thing Rust could do to make async feel less like an MVP is to ship a default executor, much like it has a default allocator.

    • Animats5 hours ago
      Agree on title. Too dramatic.

      The author seems to be obsessing about the overhead for trivial functions. He's bothered by overhead for states for "panicked" and "returned". That's not a big problem. Most useful async blocks are big enough that the overhead for the error cases disappears.

      He may have a point about lack of inlining. But what tends to limit capacity for large numbers of activities is the state space required per activity.

      • molf5 hours ago
        > Most useful async blocks are big enough that the overhead for the error cases disappears.

        Is it really though?

        In my experience many Rust applications/libraries can be quite heavy on the indirection. One of the points from the article is that contrary to sync Rust, in async Rust each indirection has a runtime cost. Example from the article:

            async fn bar(blah: SomeType) -> OtherType {
               foo(blah).await
            }
        
        I would naively expect the above to be a 'free' indirection, paying only a compile-time cost for the compiler to inline the code. But after reading the article I understand this is not true, and it has a runtime cost as well.
      • selfhoster13122 hours ago
        In my experience, it's not uncommon to have an async trait method for which many implementations are actually synchronous. For example, different tables in your DB need to perform some calculations, but only some tables reference other tables. In that case, the method needs to be async and take a handle to the DB as parameter, but many table entries can perform the calculation on their own without using the handle (or any async operation).

        This may look like a case of over-optimization, but given how many times i've seen this pattern, i assume it builds up to a lot of unnecessary fluff in huge codebases. To be clear, in that case, the concern is not really about runtime speed (which is super fast), but rather about code bloat for compilation time and binary size.

      • swiftcoder4 hours ago
        > Most useful async blocks are big enough that the overhead for the error cases disappears.

        Most useful async blocks are deeply nested, so the overhead compounds rapidly. Check the size of futures in a decently large Tokio codebase sometime

      • algesten4 hours ago
        He's optimizing for embedded no-std situation. These things do matter in constrained environments.
      • repelsteeltje3 hours ago
        > [...] That's not a big problem [...]

        Depends somewhat on your expectations, I suppose. Compared to Python, Java, sure, but Rust off course strives to offer "zero-cost" high level concepts.

        I think the critique is in the same realm of C++'s std::function. Convenience, sure, but far from zero-cost.

        • pjmlp2 hours ago
          To the point it got replaced by std::function_ref() in C++26.
          • repelsteeltje2 hours ago
            Exactly. And I guess that is also the gist of the article: async Rust needs additional TLC.
      • dathinab4 hours ago
        > Agree on title. Too dramatic.

        not just too dramatic

        given that all the things they list are

        non essential optimizations,

        and some fall under "micro optimizations I wouldn't be sure rust even wants",

        and given how far the current async is away from it's old MVP state,

        it's more like outright dishonest then overly dramatic

        like the kind of click bait which is saying the author does cares neither about respecting the reader nor cares about honest communication, which for someone wanting to do open source contributions is kinda ... not so clever

        through in general I agree rust should have more HIR/MIR optimizations, at least in release mode. E.g. its very common that a async function is not pub and in all places directly awaited (or other wise can be proven to only be called once), in that case neither `Returned` nor `Panicked` is needed, as it can't be called again after either. Similar `Unresumed` is not needed either as you can directly call the code up to the first await (and with such a transform their points about "inlining" and "asyncfns without await still having a state machine" would also "just go away"TM, at least in some places.). Similar the whole `.map_or(a,b)` family of functions is IMHO a anti-pattern, introducing more function with unclear operator ordering and removal of the signaling `unwrap_` and no benefits outside of minimal shortening a `.map(b).unwrap_or(a)` and some micro opt. is ... not productive on a already complicated language. Instead guaranteed optimizations for the kind of patterns a `.map(b).unwrap_or(a)` inline to would be much better.

  • groundzeros20155 hours ago
    Async seems like an underbaked idea across the board. Regular code was already async. When you need to wait for an async operation, the thread sleeps until ready and the kernel abstracts it away. But We didn’t like structuring code into logical threads, so we added callback systems for events. Then realized callbacks are very hard to reason about and that sequential control is better.

    So threads was the right programming model.

    Now language runtimes prefer “green threads” for portability and performance but most languages don’t provide that properly. Instead we have awkward coloring of async/non-async and all these problems around scheduling, priority, and no-preemption. It’s a worse scheduling and process model than 1970.

    • vlovich1234 hours ago
      > Regular code was already async. When you need to wait for an async operation, the thread sleeps until ready and the kernel abstracts it away

      Not really. I’ve observed async code often is written in such a way that it doesn’t maximize how much concurrency can be expressed (eg instead of writing “here’s N I/O operations to do them all concurrently” it’s “for operation X, await process(x)”). However, in a threaded world this concurrency problem gets worse because you have no way to optimize towards such concurrency - threads are inherently and inescapably too heavy weight to express concurrency in an efficient way.

      This is is not a new lesson - work stealing executors have long been known to offer significantly lower latency with more consistent P99 than traditional threads. This has been known since forever - in the early 00s this is why Apple developed GCD. Threads simply don’t provide any richer information it needs in the scheduler to the kernel about the workload and kernel threads are an insanely heavy mechanism for achieving fine grained concurrency and even worse when this concurrency is I/O or a mixed workload instead of pure compute that’s embarrassingly easily to parallelize.

      Do all programs need this level of performance? No, probably not. But it is significantly more trivial to achieve a higher performance bar and in practice achieve a latency and throughput level that traditional approaches can’t match with the same level of effort.

      You can tell async is directionally kind of correct in that io_uring is the kernel’s approach to high performance I/O and it looks nothing like traditional threading and syscalls and completion looks a lot closer to async concurrency (although granted exploiting it fully is much harder in an async world because async/await is an insufficient number of colors to express how async tasks interrelate)

      • groundzeros20154 hours ago
        I am not saying threads are the model for all programming problems. For example a dependency graph like an excel spreadsheet can be analyzed and parallelized.

        But as you observed, async/await fails to express concurrency any better. It’s also a thread, it’s just a worse implementation.

        • vlovich1234 hours ago
          That’s incorrect. Even when expressed suboptimally, it still tends to result in overall higher throughput and consistently lower latency (work stealing executors specifically). And when you’re in this world, you can always do an optimization pass to better express the concurrency. If you’ve not written it async to start with, then you’re boned and have no easy escape hatch to optimize with.
          • groundzeros20154 hours ago
            Why can’t you do the same optimization? Are you maxing out you OS system resources on thread overhead?
      • Hendrikto2 hours ago
        > threads are inherently and inescapably too heavy weight to express concurrency in an efficient way

        Your premise is wrong. There are many counterexamples to this.

        • LelouBilan hour ago
          Can you explain more ? I always heard this.
          • Hendrikto21 minutes ago
            The most promiment example is probably Go with its goroutines, but there are so many more. You can easily spawn tens of thousands of goroutines, with low overhead and great performance.
    • nananana94 hours ago
      > the thread sleeps until ready and the kernel abstracts it away.

      Sure, but once you involve the kernel and OS scheduler things get 3 to 4 orders of magnitude slower than what they should be.

      The last time I was working on our coroutine/scheduling code creating and joining a thread that exited instantly was ~200us, and creating one of our green threads, scheduling it and waiting for it was ~400ns.

      You don't need to wait 10 years for someone else to design yet another absurdly complex async framework, you can roll your own green threads/stackful coroutines in any systems language with 20 lines of ASM.

      • groundzeros20154 hours ago
        1. Why can’t we have better green threads implementations with better scheduling models?

        2. Unchecked array operations are a lot faster. Manual memory management is a lot faster. Shared memory is a lot faster.

        Usually when you see someone reach for sharp and less expressive tools it’s justified by a hot code path. But here we jump immediately to the perf hack?

        3. How many simultaneous async operations does your program have?

        • vlovich1234 hours ago
          Well, if you offload heavy compute into an async task, then usually it depends strictly on how many concurrent inputs you are given. But even something as “simple” as a performance editor benefits from this if done well - that’s why JS text editors have reasonably acceptable performance whereas Java IDEs always struggled (historically anyway since even Java has adopted green threads).
          • ptx3 hours ago
            Are you sure Java's UI issues are caused by threading and not just Swing being a glitchy pile of junk?

            For example, if you don't explicitly call the java.awt.Toolkit.sync() method after updating the UI state (which according to the docs "is useful for animation"), Swing will in my experience introduce seemingly random delays and UI lag because it just doesn't bother sending the UI updates to the window system.

          • groundzeros20154 hours ago
            You think IDEs are written in JS because of the performance benefits of the threading model?

            I thought it was because they could copy chromium.

            • vlovich1234 hours ago
              Why do you think they don’t struggle with input latency? Because the non blocking nature built into the browser model is so powerful and you cannot get that with threads.
              • groundzeros20154 hours ago
                I disagree with the premise. I cannot imagine a better latency experience than blocking loop IDEs like VS6.

                Which inputs are getting latency? The keyboard? The files?

                > the non blocking nature

                https://youtu.be/bzkRVzciAZg?si=BuBXxHTgN0OqsAhI

              • usrnm4 hours ago
                Are you sure that latency-sensitive parts are written in async JS instead of having a separate UI thread (pool)? I have no idea myself, but without knowing the details it's hard to argue. Note, that browsers themselves, are usually written in languages like C++ or Rust. They run JS, but aren't written in it
                • pjmlp2 hours ago
                  Yes they are, the UI layer is mostly JS, outside the rendering and layout engines.
                • spwa43 hours ago
                  If you implement threads and code that reacts to an input queue (e.g. PostMessage, queue_push, mq_send, ...), you've implemented (probably a bad version of) async threads. And yes, that's exactly what Windows 1.0 did and what made it great.

                  But God help you if you have to change the code. Async threads are a way to organize it and make it workable for humans.

          • PunchyHamster3 hours ago
            Maybe you remember performance of IDEs from 15 years ago because that definitely isn't my experience.
          • jcelerier28 minutes ago
            > that’s why JS text editors have reasonably acceptable performance

            Absolutely not

    • BlackFly4 hours ago
      I think that callbacks are actually easier to reason about:

      When it comes time to test your concurrent processing, to ensure you handle race conditions properly, that is much easier with callbacks because you can control their scheduling. Since each callback represents a discrete unit, you see which events can be reordered. This enables you to more easily consider all the different orderings.

      Instead with threads it is easy to just ignore the orderings and not think about this complexity happening in a different thread and when it can influence the current thread. It isn't simpler, it is simplistic. Moreover, you cannot really change the scheduling and test the concurrent scenarios without introducing artificial barriers to stall the threads or stubbing the I/O so you can pass in a mock that you will then instrument with a callback to control the ordering...

      The problem with callbacks is that the call stack when captured isn't the logical callstack unless you are in one of the few libraries/runtimes that put in the work to make the call stacks make sense. Otherwise you need good error definitions.

      You can of course mix the paradigms and have the worst of both worlds.

      • groundzeros20154 hours ago
        I agree. I don’t think callbacks are an underbaked language feature.
    • usrnm4 hours ago
      The problem comes from trying to sit on both chairs: we want async but want to be able to opt out. This is what causes most of the ugliness, including function colouring. Just look at golang, where everything is async with no way to change it, it's great. It's, probably, not well-suited for things like microcontrollers, where every byte matters, but if you can afford the overhead, it's so much better than Rust async. Before async Rust was an interesting and reasonable language, now it's just a hot mess that makes your eyes bleed for no reason.
      • vanderZwan3 hours ago
        > It's, probably, not well-suited for things like microcontrollers, where every byte matters, but if you can afford the overhead, it's so much better than Rust async.

        There is one hill I'll die on, as far as programming languages go, which is that more people should study Céu's structured synchronous concurrency model. It specifically was designed to run on microcontrollers: it compiles down to a finite state machine with very little memory overhead (a few bytes per event).

        It has some limitations in terms of how its "scheduler" scales when there are many trails activated by the same event, but breaking things up into multiple asynchronous modules would likely alleviate that problem.

        I'm certain a language that would suppprt the "Globally Asynchronous, Locally Synchronous" (GALS) paradigm could have their cake and eat it too. Meaning something that combines support for a green threading model of choice for async events, with structured local reactivity a la Céu.

        F'Santanna, the creator of Céu, actually has been chipping away at a new programming language called Atmos that does support the GALS paradigm. However, it's a research language that compiles to Lua 5.4. So it won't really compete with the low-level programming languages there.

        [0] https://ceu-lang.org/

        [1] https://github.com/atmos-lang/atmos

      • PunchyHamster3 hours ago
        Everything is not async in Go.

        If your threads are "free" you can just run 400 copies of a synchronous code and blocking in one just frees the thread to work on other. async within same goroutine is still very much opt in (you have to manually create goroutine that writes to channel that you then receive on), it just isn't needed where "spawn a thread for each connecton" costs you barely few kb per connection.

        • jeremyjh2 hours ago
          What GP meant - what everyone means when they say this - is that goroutines are always M:N threading and so there is no such thing as function coloring. In Rust to get M:N threading you have to use async and in practice every library you use has to use async. Hence function coloring, and two separate ecosystems of libraries in the same language.
      • baq3 hours ago
        > not well-suited for things like microcontrollers, where every byte matters

        except when a RAM fetch is so expensive a load is basically an async call - and it's a single machine code instruction at the same time

    • pkolaczk4 hours ago
      Threads are neither better or worse than async+callbacks. They are different. There are problems which map nicely to threads and there are problems which are much nicer to express with async.
      • groundzeros20154 hours ago
        Such as? The entire premise of async is that callbacks were a mistake because they broke sequential reasoning and control.

        Every explanation of the feature starts with managing callback hell.

        • repelsteeltje3 hours ago
          Beware, they are different concepts.

          Threads offer concurrent execution, async (futures) offer concurrent waiting. Loosely speaking, threads make sense for CPU bound problems, while async makes sense for IO bound problems.

        • codedokode3 hours ago
          The callbacks should be just hidden from programmer, that's what async/await are for.
    • swiftcoder5 hours ago
      > So threads was the right programming model.

      For problems that aren't overly concerned with performance/memory, yes. You should probably reach for threads as a default, unless you know a priori that your problem is not in this common bucket.

      Unfortunately there is quite a lot of bookkeeping overhead in the kernel for threads, and context switches are fairly expensive, so in a number of high performance scenarios we may not be able to afford kernel threading

      • groundzeros20154 hours ago
        In that sentence I’m referring to the abstract idea of a thread of execution as a model of programming, not OS threads. A green thread implementation could do it too.

        But what you said about kernel implementation is true. But are we really saying that the primary motivation for async/await is performance? How many programmers would give that answer? How many programs are actually hitting that bottleneck?

        Doesn’t that buck the trend of every other language development in the past 20 years, emphasizing correctness and expressively over raw performance?

        • nchie4 hours ago
          > But are we really saying that the primary motivation for async/await is performance?

          Of course - what else would it be? The whole async trend started because moving away from each http request spawning (or being bound to) an OS thread gave quite extreme improvements in requests/second metrics, didn't it?

          • lukaslalinskyan hour ago
            It was not for performance reasons, but for scaling up.
            • pjc50an hour ago
              That's the same thing?
          • groundzeros20154 hours ago
            I agree. Managing many http requests or responses was a motivating problem.

            What I question is whether 1. Most programs resemble that, so that they make it an invasive feature of every general purpose language. 2. Whether programmers are making a conscious choice because they ruled out the perf overhead of the simpler model we have by default.

            • swiftcoder4 hours ago
              That is why we have the function colouring problem and a split ecosystem in the first place - if it were obviously better in all cases, we'd make async the default, and get rid of the split altogether (and there are languages, like Erlang, that fall on this side of the fence)
        • swiftcoder4 hours ago
          > But are we really saying that the primary motivation for async/await is performance?

          The original motivation for not using OS threads was indeed performance. Async/await is mostly syntax sugar to fix some of the ergonomic problems of writing continuation-based code (Rust more or less skipped the intermediate "callback hell" with futures that Javascript/Python et al suffered through).

          • PunchyHamster3 hours ago
            In some languages, yes, in others (js/python) async is just workaround about not having proper threading.
            • swiftcoderan hour ago
              Python used multiple threads to handle I/O long before async/await was a glimmer in anyone's mind (despite the GIL). nodejs is one of the very few languages I can think of that was born single-threaded and used an asynchronous runtime from the get-go
        • sureglymop4 hours ago
          Importantly though, performance might be worse depending on use case and program. Specifically with scheduling in user space it can negatively impact branch prediction as your CPU is already hyper optimized for doing things differently.

          It's all nuanced and what to choose requires careful evaluation.

    • dgellowan hour ago
      You don’t have threads on embedded, but you want a way to express concurrent waiting. Different problems altogether
    • codedokode4 hours ago
      As I understand, "green threads" are also expensive, for example you either need to allocate a large stack for each "thread", or hook stack allocation to grow the stack dynamically (like Go does), and if you grow the stack, you might have to move it and cannot have pointers to stack objects.
      • lukaslalinskyan hour ago
        Green threads are fine for large servers with memory overcommit. Even with static stack sizes, you get benefits over OS threads due to the simpler scheduling. But the post was about embedded and green threads really suck there. Only using as much stack as you need for the task is the perfect solution for embedded systems.
      • kgeist2 hours ago
        >and if you grow the stack, you might have to move it

        Most stacks are tiny and have bounded growth. Really large stacks usually happen with deep recursion, but it's not a very common pattern in non-functional languages (and functional languages have tail call optimization). OS threads allocate megabytes upfront to accommodate the worst case, which is not that common. And a tiny stack is very fast to copy. The larger the stack becomes, the less likely it is to grow further.

        >cannot have pointers to stack objects

        In Go, pointers that escape from a function force heap allocation, because it's unsafe to refer to the contents of a destroyed stack frame later on in principle. And if we only have pointers that never escape, it's relatively trivial to relocate such pointers during stack copying: just detect that a pointer is within the address range of the stack being relocated and recalculate it based on the new stack's base address.

      • PunchyHamster3 hours ago
        works fine in Go.

        Yes, you're not getting Rust performance (tho good part of it is their own compiler vs using all LLVM goodness) but performance is good enough and benefits for developers are great, having goroutines be so cheap means you don't even need to do anything explicitly async to get what you want

        • aw16211072 hours ago
          Rust chose a different design space for their async implementation though, so what works well for Go wouldn't work well for Rust. In particular, the Rust devs wanted zero-cost FFI that external code doesn't need to know about, which precludes Go-like green threads.
    • pjmlp2 hours ago
      Proper modern languages offer both, you can keep your threads and reach out to async only when it makes sense to do.

      Now the languages that don't offer choice is another matter.

    • the__alchemistan hour ago
      What is kernel in this context?
    • hacker_homie4 hours ago
      I’m just waiting for them to try co-operative multithreading again.
    • K0nserv2 hours ago
      I think you are correct, in so far that often N:M threading is overkill for the problem at hand. However, some IO bound problems truly do require it. I haven't kept up with the details, but AFAIK the fallout from Spectre and Meltdown also means context switches are more expensive than they were historically, which is another downside with regular threads.

      I also want to address something that I've seen in several sub-threads here: Rust's specific async implementation. The key limitation, compared to the likes of Go and JS, is that Rust attempts to implement async as a zero-cost abstraction, which is a much harder problem than what Go and JS does. Saying some variant of "Rust should just do the same thing as Go", is missing the point.

  • conaclos4 hours ago
    I recently started working with Rust async. The main issue I am currently facing is code duplication: I have to duplicate every function that I want to support both asynchronous and blocking APIs. This could be great to have a `maybe-async`. I took a look at the available crates to work around this (maybe-async, bisync), but they all have issues or hard limitations.
    • K0nserv3 hours ago
      There is work happening on keyword generics[0], which would let a function be generic over keywords like `async` and `const`.

      For now the best option to write code that wants to live in both worlds is sans-io. Thomas Eizinger at Fireguard has written a good article about this[1] pattern. Not only does it nicely solve the sync/async issue, but it also makes testing easier and opens the door to techniques like DST[2]

      I have my own writing on the topic[3], which highlights that the problem is wider than just async vs sync due to different executors.

      0: https://github.com/rust-lang/effects-initiative

      1: https://www.firezone.dev/blog/sans-io

      2: https://notes.eatonphil.com/2024-08-20-deterministic-simulat...

      3: https://hugotunius.se/2024/03/08/on-async-rust.html

      • ignoreusernames2 hours ago
        I may have missed something, but how does “sans-io” deal with CPU heavy code? For example, if there’s some heavy decoding/encoding required on the data? Does the event loop only drive the network side and the heavy part is done after the loop is finished?
        • K0nserv40 minutes ago
          This is a great question and there isn't a definitive answer provided in the sources I linked.

          Broadly I think there are three approaches:

          1. For frequent and small CPU heavy tasks, just run them on the IO threads. As long as you don't leave too long between `.await` points (~10ms) it seems to work okay.

          2. Run your sans-io code on a dedicated CPU thread and do IO from an async runtime. This introduces overhead that needs to be weighed against the amount of CPU work.

          3. Have the sans-io code output something like `Output::DoHeavyCompute { .. }` and later feed the result back as `Input::HeavyComputeResult { .. }`, in the middle run the work on a thread pool.

      • paavohtlan hour ago
        Considering the latest commits and issues in effects-initiative are about 2 years old, the keyword generics initiative seems effectively dead.
    • albertzeyer4 hours ago
    • small_scombrus4 hours ago
      It'll depend immensely on what you're actually doing, but if it's simple enough you may be able to make a macro that subs out the types & awaits
  • dragochat14 minutes ago
    what's the modern "absolute beginner's guide to async in Rust" - ideally something dense that can bring someone motivated from beginner to expert in ~1 week of intense hacking on it?
  • InfinityByTen3 hours ago
    I like this article already because it took me to the goals of Rust for 2026. We use the language in our team, but we haven't needed to go very deep to do the stuff we need. Yet, I really enjoy witnessing the development of a language from ground up with so much community feedback.

    I somehow miss noticing that in C++ and I have no idea how it is working in other domains.

    My only gripe is that a lot of it is feeling a bit kick-starter-y, with each of the goals needing specific funding. Is that the best model we've found so far?

    • repelsteeltje3 hours ago
      > I somehow miss noticing that in C++ and I have no idea how it is working in other domains.

      There seems to be some consensus even within the C++ ISO committee that the evolution process of that language is somewhat broken, mostly due to its size and the way it is organized.

      > My only gripe is that a lot of it is feeling a bit kick-starter-y, with each of the goals needing specific funding. Is that the best model we've found so far?

      Sadly, this seems to be the way things go once a technology catches on, commercially. Can't blame large donors for sponsoring only the parts they are interested in. Fortunately, considerable funding of TweedeGolf comes from (Dutch) government, I think.

    • diondokter3 hours ago
      In open source I guess there's two types of work: 1. features 2. maintenance

      You can 'sell' new features. They cost money to create, but they solve real problems. Those problems also cost money and if that's more than the cost of creating the feature, companies are willing to put in money (generally).

      Maintenance is harder. But there are now some maintainer funds! Like the one from RustNL: https://rustnl.org/maintainers/ These are broader ongoing work and backed by many orgs chipping in a little bit.

      Idk if it's the best model, but at least it seems to kinda work

  • _alphageekan hour ago
    The duplicate-state collapse (hoisting the match out of the await branches like in his process_command example) is the single easiest pattern anyone can apply to existing async code today. No compiler work needed, just a refactor.
    • zozbot23435 minutes ago
      At the very least, you'd want to have a custom lint that can surface the places where it's applicable. That's pretty close to compiler work.
  • hacker_homie4 hours ago
    This is the type of ugly but necessary discussions that have been happening in c++ for a while.

    I never really liked the viral nature of async in rust when it was introduced.

    I wish rust the best of luck and with more people like this rust could have a brighter future.

  • Panzerschrekan hour ago
    > Futures aren't (trivially) inlined

    In my programming language I wrote custom pass for inlining async function calls within other async functions. It generally works and allows to remove some boilerplate, but it increases result binary size a lot.

    Technically Rust can do the same.

  • ozgrakkurt4 hours ago
    Does this kind of thing make noticeable difference when applied to more complicated async functions?

    Examples in the blog seem too simple make any conclusions

    • diondokter4 hours ago
      Hi, author here. I mention in the blog that I've tried to quickly hack two of the simplest optimizations in the compiler and it resulted in 2%-5% binary size savings in real embedded (async) codebases. And a quick and probably deeply flawed synthetic benchmark on the desktop showed a 3% perf increase.

      So yes, it does really matter. Keep in mind that optimizations stack. We're preventing LLVM from doing it's thing. So if we make the futures themselves smaller, LLVM will be able to optimize more. So small changes really compound.

      • ozgrakkurtan hour ago
        Saw that but couldn't find what code it gives that improvement on. Is it some embedded application written with Embassy?
        • diondokteran hour ago
          Yes, but I can't share the codebases since they're our client's and proprietary. But there aren't a lot of big embedded codebases that are also open source
  • Havoc2 hours ago
    This has been on my mind lately too with the talk of the new CPUs. Zen 7 sounds like it'll be a beast & coding against 1 out of dozens of cores would be a pity
  • emilfihlman33 minutes ago
    Rust is a passing faux, safe C will just overtake it.
  • whazor3 hours ago
    Async Rust on small embedded chips like ESP32 feels revolutionary. This project looks promising.
  • jagged-chiselan hour ago
    Response to title: so you’re saying it’s viable
  • feverzsj3 hours ago
    There are much more problems, like async drop.
  • micoul812 hours ago
    great article
  • DeathArrow3 hours ago
    I like it more how Zig is approaching async with the new IO. It avoids function coloring.
  • forrestthewoods4 hours ago
    Love Rust. They simply missed the mark with async. Swing and a miss.

    The risk they took was very calculated. Unfortunately they’re bad at math and chose the wrong trade-offs.

    Ah well. Shit happens.

    • lionkor4 hours ago
      I think Rust has a pretty solid async implementation, compared to other systems languages. I struggle to point out another systems language with a working and actually used async implementation.
    • swiftcoder4 hours ago
      > Unfortunately they’re bad at math and chose the wrong trade-offs

      They chose the exact same tradeoffs as C++'s async/await (and the same overall model as Python/NodeJS), so I'm not sure what that says about programming as a whole.

      • nromiun4 hours ago
        Async in Rust and C++ is nothing like it is in Python or NodeJS. Choose your own runtime is a very different model than having a default one.

        Not to mention Tokio (most popular runtime for Rust) is multi-threaded by default. So you have to deal with multithreading bugs as well as normal async ones. That is not the case with most async languages. For example both Python and NodeJS use a single thread to execute async code.

        • swiftcoder3 hours ago
          > Async in Rust and C++ is nothing like it is in Python or NodeJS. Choose your own runtime is a very different model than having a default one.

          Python still has pluggable eventloops - this is sort of mandatory to interact with weird things like GUI toolkits, and Python's standard event loop was standardised pretty late in the game. Early on there was even an ecosystem split between Twisted and competing event loops implementations.

          > For example both Python and NodeJS use a single thread to execute async code

          I'd argue this is more a historical artefact of how the languages functioned before futures were introduced, rather than an inherent limitation.

          • nromiun2 hours ago
            It is an inherent limitation. Multithreading is not free after all. One of the big pros of async programming is the concurrency you get within a single thread. When you make the async runtime multithreaded by default (like Tokio) you don't get this advantage anymore.
            • swiftcoderan hour ago
              You can put tokio in single-threaded mode if you prefer - it's an explicit performance tradeoff. The multithreaded work-stealing executor has higher throughput at the expense of needing more synchronisation.

              Or you can schedule your thread-local tasks in a LocalSet to run them all on the owning thread, while keeping the other threads around to handle tasks that are intentionally parallel.

              The general theme here is that tokio (and C++ equivalents) provide you the flexibility to do more things than the native Python/Node runtime does (and yes, the defaults take advantage of this). But the underlying intention is the same (and post-GIL we expect to see some movement in this direction on the Python front as well).

  • slopinthebag4 hours ago
    It's so funny that people will do anything to hate on Rust, including nitpicking a few bytes of overhead for a future while they reach for an entire thread or runtime to handle async in their favourite language.
    • berkes4 hours ago
      I know the people and the company behind this article. They do anything but "hate on Rust".

      You could've deduced that from the fact that someone who puts this amount of energy in a detailed article about intricacies of an area of "foo", quite certainly does not "hate on foo".

      • slopinthebag4 hours ago
        Not the article, the comments here man.

        The article is fine besides the bait title.

    • lionkor4 hours ago
      It's more that I and people I know love Rust, and enjoy it, and want it to be better. I want it to be relentlessly optimized.
    • jjgreenan hour ago
      That's a bit rich given the abuse that Rust evangelists dish out to every other language in the world.
    • rnijveld4 hours ago
      You realize this article talks about Rust on embedded hardware specifically, where you don’t have threads or big runtimes? There is no hate going on here either, just attempts to make things better. Might I suggest you click through to the homepage and I think you’ll figure out the rest.
    • gspr4 hours ago
      I _love_ Rust and use it whenever I can. I still find the comments in here to be quite appropriate. Async Rust leaves me with a (subjective!) feeling that something isn't quite right. Not that I know how it _should_ be, but that feeling is very different from the non-async parts of the language that almost always leaves me with a warm fuzzy feeling of joy.

      I don't know enough about the domain to be objectively helpful, so it's all wishy-washy feelings on my part. I keep reaching for orchestrating things with threads in Rust where most people would probably reach for async these days. The only language where I've felt fine embracing the blessed async system is Haskell and its green threads (which I understand come with their own host of problems).

    • MrBuddyCasino4 hours ago
      Nobody seriously tries to run Golang or Java on an MCU. But they do run Rust code.