[HN Gopher] Looking at Unity made me understand the point of C++...
       ___________________________________________________________________
        
       Looking at Unity made me understand the point of C++ coroutines
        
       Author : ingve
       Score  : 150 points
       Date   : 2026-03-21 23:19 UTC (3 days ago)
        
 (HTM) web link (mropert.github.io)
 (TXT) w3m dump (mropert.github.io)
        
       | abcde666777 wrote:
       | More broadly the dimension of time is always a problem in
       | gamedev, where you're partially inching everything forward each
       | frame and having to keep it all coherent across them.
       | 
       | It can easily and often does lead to messy rube goldberg
       | machines.
       | 
       | There was a game AI talk a while back, I forget the name
       | unfortunately, but as I recall the guy was pointing out this
       | friction and suggesting additions we could make at the
       | programming language level to better support that kind of time
       | spanning logic.
        
         | repelsteeltje wrote:
         | > There was a game AI talk a while back, I forget the name
         | unfortunately, but as I recall the guy was pointing out this
         | friction and suggesting additions we could make at the
         | programming language level to better support that kind of time
         | spanning logic.
         | 
         | Sounds interesting. If it's not too much of an effort, could
         | you dig up a reference?
        
           | abcde666777 wrote:
           | You're in luck - it's the first talk at this link, "The
           | Polling Problem":
           | https://www.gdcvault.com/play/1018040/Architecture-Tricks-
           | Ma...
           | 
           | Mind you my memory may have distorted it a little beyond what
           | it was, but it's loosely on the topic!
        
         | syncurrent wrote:
         | This timing additions to a language is also at the core of
         | imperative synchronous programming languages like Este rel, Ceu
         | or Blech.
        
         | manoDev wrote:
         | This is more evident in games/simulations but the same problem
         | arises more or less in any software: batch jobs and DAGs,
         | distributed systems and transactions, etc.
         | 
         | This what Rich Hickey (Clojure author) has termed "place
         | oriented programming", when the focus is mutating memory
         | addresses and having to synchronize everything, but failing to
         | model time as a first class concept.
         | 
         | I'm not aware of any general purpose programming language that
         | successfully models time explicitly, Verilog might be the
         | closest to that.
        
           | gopher_space wrote:
           | > I'm not aware of any general purpose programming language
           | that successfully models time explicitly
           | 
           | Step 1, solve "time" for general computing.
           | 
           | The difficulty here is that our periods are local out of both
           | necessity and desire; we don't fail to model time as a first
           | class concept, we bring time-as-first-class with us and then
           | attempt to merge our perspectives with varying degrees of
           | success.
           | 
           | We're trying to rectify the observations of Zeno, a
           | professional turtle hunter, and a track coach with a
           | stopwatch when each one has their own functional definition
           | of time driven by intent.
        
       | cherryteastain wrote:
       | Not an expert in game development, but I'd say the issue with C++
       | coroutines (and 'colored' async functions in general) is that the
       | whole call stack must be written to support that. From a
       | practical perspective, that must in turn be backed by a
       | multithreaded event loop to be useful, which is very difficult to
       | write performantly and correctly. Hence, most people end up using
       | coroutines with something like boost::asio, but you can do that
       | only if your repo allows a 'kitchen sink' library like Boost in
       | the first place.
        
         | spacechild1 wrote:
         | ASIO is also available outside of boost!
         | https://github.com/chriskohlhoff/asio
        
           | lionkor wrote:
           | For anyone wondering; this isn't a hack, that's the same
           | library, just as good, just without boost dependencies.
        
             | spacechild1 wrote:
             | Thanks for pointing this out! This may not obvious not
             | everybody.
             | 
             | Also, this is not some random GitHub Repo, Chris Kohlhoff
             | is the developer of ASIO :)
        
         | pjc50 wrote:
         | Much of the original motivation for async was for _single_
         | threaded event loops. Node and Python, for example. In C# it
         | was partly motivated by the way Windows handles a  "UI thread":
         | if you're using the native Windows controls, you can only do so
         | from one thread. There's quite a bit of machinery in there
         | (ConfigureAwait) to control whether your async routine is run
         | on the UI thread or on a different worker pool thread.
         | 
         | In a Unity context, the engine provides the main loop and the
         | developer is writing behaviors for game entities.
        
         | spacechild1 wrote:
         | > that must in turn be backed by a multithreaded event loop to
         | be useful
         | 
         | Why? You can just as well execute all your coroutines on a
         | single thread. Many networking applications are doing fine with
         | just use a single ASIO thread.
         | 
         | Another example: you could write game behavior in C++
         | coroutines and schedule them on the thread that handles the
         | game logic. If you want to wait for N seconds inside the
         | coroutine, just yield it as a number. When the scheduler
         | resumes a coroutine, it receives the delta time and then
         | reschedules the coroutine accordingly. This is also a common
         | technique in music programming languages to implement musical
         | sequencing (e.g. SuperCollider)
        
         | inetknght wrote:
         | > _From a practical perspective, that must in turn be backed by
         | a multithreaded event loop to be useful_
         | 
         | Multithreaded? Nope. You can do C++ coroutines just fine in a
         | single-threaded context.
         | 
         | Event loop? Only if you're wanting to do IO in your coroutines
         | and not block other coroutines while waiting for that IO to
         | finish.
         | 
         | > _most people end up using coroutines with something like
         | boost::asio_
         | 
         | Sure. But you don't have to. Asio is available without the
         | kitchen sink: https://think-async.com/Asio/
         | 
         | Coroutines are actually really approachable. You don't _need_
         | boost::asio, but it certainly makes it a lot easier.
         | 
         | I recommend watching Daniela Engert's 2022 presentation,
         | Contemporary C++ in Action:
         | https://www.youtube.com/watch?v=yUIFdL3D0Vk
        
           | Davidbrcz wrote:
           | I use asio at work for coroutine. It's one of the most opaque
           | library I've ever used. The doc is awful and impenetrable.
           | 
           | The most helpful resource about it is a guy on stackoverflow
           | (sehe). No idea how to get help once SO will have closed
        
             | astrange wrote:
             | Ask Claude Code to write a manual for it.
        
         | nitwit005 wrote:
         | > but I'd say the issue with C++ coroutines (and 'colored'
         | async functions in general) is that the whole call stack must
         | be written to support that.
         | 
         | You can call a function that makes use of coroutines without
         | worrying about it. That's the core intent of the design.
         | 
         | That is, if you currently use some blocking socket library, we
         | could replace the implementation of that with coroutine based
         | sockets, and everything should still work without other code
         | changes.
        
         | hrmtst93837 wrote:
         | They don't need a multithreaded event loop. Single-threaded
         | schedulers cover plenty of game-style work without hauling in
         | Boost, and the uglier part is that async colors the API surface
         | and control flow in ways that make refactors annoying and big
         | legacy codebases harder to reason about.
        
       | pjc50 wrote:
       | Always jarring to see how Unity is stuck on an ancient version of
       | C#. The use of IEnumerable as a "generator" mechanic is quite a
       | good hack though.
        
         | repelsteeltje wrote:
         | Not too different from C++'s iterator interface for generators,
         | I guess.
        
         | debugnik wrote:
         | Not _that_ ancient, they just haven 't bothered to update their
         | coroutine mechanism to async/await. The Stride engine does it
         | with their own scheduler, for example.
         | 
         | Edit: Nevermind, they eventually bothered.
        
           | nananana9 wrote:
           | Unity has async too [1]. It's just that in a rare display of
           | sanity they chose to not deprecate the IEnumerator stuff.
           | 
           | [1] https://docs.unity3d.com/6000.3/Documentation/ScriptRefer
           | enc...
        
             | debugnik wrote:
             | Oh I totally missed this, thanks! I was overly confident
             | they wouldn't have bothered, given how long it was taking.
             | The last time I used Unity was 2022.3, which was apparently
             | the last version without Awaitable.
        
           | Rohansi wrote:
           | It's ancient. The latest version of Unity only partially
           | supports C# 9. We're up to C# 14 now. But that's just the
           | language version. The Mono runtime is only equivalent to .NET
           | Framework 4.8 so all of the standard library improvements
           | since .NET (Core) are missing. Not directly related to age
           | but it's performance is also significantly worse than .NET.
           | And Unity's garbage collector is worse than the default one
           | in Mono.
        
             | debugnik wrote:
             | The runtime is absolutely ancient, but I think the version
             | number says more about C#'s churn than about how outdated
             | the language version is. Take my opinion on C# with a grain
             | of salt, though, I was an F#-er until the increasing
             | interop pains forced me to drop it.
        
               | Rohansi wrote:
               | There were also a lot of performance improvements to .NET
               | over the last few years.
        
         | tyleo wrote:
         | Unity is currently on C# 9 and that IEnumerable trick is no
         | longer needed in new codebases. async is properly supported.
        
         | ahoka wrote:
         | IIRC generators and co-routines are equivalent in a sense that
         | you can implement one with the other.
        
           | Sharlin wrote:
           | Generators are a subset of coroutines that only yield data in
           | one direction. Full coroutines can also receive more input
           | from the caller at every yield point.
        
         | Philip-J-Fry wrote:
         | >The use of IEnumerable as a "generator" mechanic is quite a
         | good hack though.
         | 
         | Is that a hack? Is that not just exactly what IEnumerable and
         | IEnumerator were built to do?
        
           | jayd16 wrote:
           | It feels hacky because you have to (had to?) use it as the
           | async/await tool and because of that the types you're
           | generating and how they are handled is a huge mess.
           | 
           | Really you're generating the vague concept of a yield
           | instruction but you can return other coroutines that are
           | implicitly run and nest your execution... Because of this you
           | can't wait less than a frame so things are often needlessly
           | complicated and slow.
           | 
           | It's like using a key to jam a door shut. Sure a key is for
           | keeping doors closed but...
        
         | Deukhoofd wrote:
         | Thankfully they are actively working towards upgrading, Unity
         | 6.8 (they're currently on 6.4) is supposed to move fully
         | towards CoreCLR, and removing Mono. We'll then finally be able
         | to move to C# 14 (from C# 9, which came out in 2020), as well
         | as use newer .NET functionality.
         | 
         | https://discussions.unity.com/t/coreclr-scripting-and-ecs-st...
        
           | Rohansi wrote:
           | One annoying piece of Unity's CoreCLR plan is there is no
           | plan to upgrade IL2CPP (Unity's AOT compiler) to use a better
           | garbage collector. It will continue to use Boehm GC, which is
           | so much worse for games.
        
             | pjc50 wrote:
             | Why wouldn't they use the GC that comes with the dotnet AOT
             | runtime?
        
               | pjmlp wrote:
               | Probably because the AOT runtime doesn't run on game
               | consoles, straight out of the box.
               | 
               | Capcom has their own fork of .NET for the Playstation,
               | for example.
               | 
               | I don't know what kind of GC they implemented.
        
               | Rohansi wrote:
               | They just haven't announced any plans to do so yet. They
               | might one day.
               | 
               | They will not be using .NET AOT probably ever though.
               | Unity's AOT basically supports full C# (reflection etc)
               | while .NET opted to restrict it and lean more on
               | generated code.
        
           | pjmlp wrote:
           | For several years now, I wonder if it will ever happen.
        
             | Deukhoofd wrote:
             | Well, this is at least an update from earlier this month
             | with a clear roadmap of how they're going to get there.
             | There's hope!
        
       | Joker_vD wrote:
       | Simon Tatham, author of Putty, has quite a detailed blog post [0]
       | on using the C++20's coroutine system. And yep, it's a _lot_ to
       | do on your own, C++26 really ought to give us some pre-built
       | templates /patterns/scaffolds.
       | 
       | [0]
       | https://web.archive.org/web/20260105235513/https://www.chiar...
        
         | zozbot234 wrote:
         | People love to complain about Rust async-await being too
         | complicated, but somehow C++ manages to be even worse. C++
         | never disappoints!
        
           | 01HNNWZ0MV43FF wrote:
           | async is simply a difficult problem, and I think we'll find
           | irreducible complexity there. Sometimes you are just doing 2
           | or 3 things at once and you need a hand-written state machine
           | with good unit tests around it. Sometimes you can't just glue
           | 3 happy paths together into CSP and call it a day.
        
             | rafram wrote:
             | Languages like Swift do manage to make it much simpler. The
             | culture guiding Rust design pretty clearly treats
             | complexity as a goal.
        
             | quietbritishjim wrote:
             | Using structured concurrency [1] as introduced in Python
             | Trio [2] genuinely does help write much simpler concurrent
             | code.
             | 
             | Also, as noted in that Simon Tatham article, Python makes
             | choices at the language level that you have to fuss over
             | yourself in C++. Given how different Trio is from asyncio
             | (the async library in Python's standard library), it seems
             | to me that making some of those basic choices wasn't
             | actually that restrictive, so I'd guess that a lot of C++'s
             | async complexity isn't that necessary for the problem.
             | 
             | [1] https://vorpus.org/blog/notes-on-structured-
             | concurrency-or-g...
             | 
             | [2] https://trio.readthedocs.io/en/stable/
        
             | jujube3 wrote:
             | It's quite simple in Golang.
        
           | jandrewrogers wrote:
           | I find C++ coroutines to be well-designed. Most of the
           | complexity is intrinsic because it tries to be un-
           | opinionated. It allows precise control and customization of
           | almost every conceivable coroutine behavior while still
           | adhering to the principle of zero-cost abstractions.
           | 
           | Most people would prefer opinionated libraries that allow
           | them to not think about the design tradeoffs. The core
           | implementation is targeted at efficient creation of
           | opinionated abstractions rather than providing one. This is
           | the right choice. Every opinionated abstraction is going to
           | be poor for some applications.
        
             | fooker wrote:
             | C++ standards follow a tick-tock schedule for complex
             | features.
             | 
             | For the `tick`, the core language gets an un-opinionated
             | iteration of the feature that is meant for compiler
             | developers and library writers to play with. (This is why
             | we sometimes see production compilers lagging behind in
             | features).
             | 
             | For the `tock`, we try to get the standard library improved
             | with these features to a realistic extent, and also fix
             | wrinkles in the primary idea.
             | 
             | This avoids the standard library having to rely on any
             | compiler magic (languages like swift are notorious for
             | this), so in practice all libraries can leverage the
             | language to the same extend.
             | 
             | This pattern has been broken in a few instances
             | (std::initializer_list), and those have been widely
             | considered to have been missteps.
        
         | matt_d wrote:
         | See also C++ coroutines resources (posts, research, software,
         | talks):
         | https://gist.github.com/MattPD/9b55db49537a90545a90447392ad3...
        
           | ZoomZoomZoom wrote:
           | For a layperson it's clear that it's either "Writings" and
           | "Talks", or "Readings" and "'Listenings", but CPP profeciency
           | is in an inverse relation with being apt in taxonomy, it
           | looks like.
           | 
           | Thanks for the list.
        
       | nananana9 wrote:
       | You can roll stackful coroutines in C++ (or C) with 50-ish lines
       | of Assembly. It's a matter of saving a few registers and
       | switching the stack pointer, minicoro [1] is a pretty good C
       | library that does it. I like this model a lot more than C++20
       | coroutines:
       | 
       | 1. C++20 coros are stackless, in the general case every async
       | "function call" heap allocates.
       | 
       | 2. If you do your own stackful coroutines, every function can
       | suspend/resume, you don't have to deal with colored functions.
       | 
       | 3. (opinion) C++20 coros are very tasteless and "C++-design-
       | commitee pilled". They're very hard to understand, implement,
       | require the STL, they're very heavy in debug builds and you'll
       | end up with template hell to do something as simple as
       | Promise.all
       | 
       | [1] https://github.com/edubart/minicoro
        
         | pjc50 wrote:
         | > You can roll stackful coroutines in C++ (or C) with 50-ish
         | lines of Assembly
         | 
         | I'm not normally keen to "well actually" people with the C
         | standard, but .. if you're writing in assembly, you're not
         | writing in C. And the obvious consequence is that it stops
         | being portable. Minicoro only supports three architectures.
         | Granted, those are the three most popular ones, but other
         | architectures exist.
         | 
         | (just double checked and it doesn't do Windows/ARM, for
         | example. Not that I'm expecting Microsoft to ship full
         | conformance for C++23 any time soon, but they have at least
         | some of it)
        
           | manwe150 wrote:
           | Boost has stackful coroutines. They also used to be in posix
           | (makecontext).
        
           | fluoridation wrote:
           | I think what they meant is that that what it takes to add
           | coroutines support to a C/++ program. Adding it to, say, Java
           | or C# is much more involved.
        
           | blacklion wrote:
           | There is no "Linux/ARM[64]". But there are "Raspberry Pi" and
           | "RISC-V". I don't know such OSes, to be honest :-)
           | 
           | This support table is complete mess. And saying "most
           | platforms are supported" is too optimistic or even cocky.
        
           | giancarlostoro wrote:
           | > Not that I'm expecting Microsoft to ship full conformance
           | for C++23 any time soon,
           | 
           | They are actively working on it for their VS2026 C++
           | compiler. I think since 2017 or so they've kept up with C++
           | standards reasonably? I'm not a heavy C++ guy, so maybe I'm
           | wrong, but my understanding is they match the standards.
        
           | ndiddy wrote:
           | Looking at the repo, it falls back to Windows fibers on
           | Windows/ARM. If you'd like a coroutine with more backends,
           | I'm a fan of libco: https://github.com/higan-emu/libco/ which
           | has assembly backends for x86, amd64, ppc, ppc-64, arm, and
           | arm64 (and falls back to setjmp on POSIX platforms and fibers
           | on Windows). Obviously the real solution would be for the C
           | or C++ committees to add stackful coroutines to the standard,
           | but unless that happens I would rather give up support for
           | hppa or alpha or 8-bit AVR or whatever than not be able to
           | use stackful corountines.
        
             | gpderetta wrote:
             | A proposal to add stackfull coroutines has been around
             | forever and gets updated at every single mailing.
             | Unfortunately the authors don't really have backing from
             | any major company.
        
           | audidude wrote:
           | > I'm not normally keen to "well actually" people with the C
           | standard, but .. if you're writing in assembly, you're not
           | writing in C.
           | 
           | These days on Linux/BSD/Solaris/macOS you can use
           | makecontext()/swapcontext() from ucontext.h and it will turn
           | out roughly the same performance on important architectures
           | as what everyone used to do with custom assembly. And you
           | already have fiber functions as part of the Windows API to
           | trampoline.
           | 
           | I had to support a number of architectures in libdex for
           | Debian. This is GNOME code of course, which isn't everyone's
           | cup of C. (It also supports
           | BSDs/Linux/macOS/Solaris/Windows).
           | 
           | * https://packages.debian.org/sid/libdex-1-1
           | 
           | * https://gitlab.gnome.org/GNOME/libdex
        
             | gpderetta wrote:
             | Unfortunately swap context requires saving and restoring
             | the signal mask, which, at least on Linux, requires a
             | syscall so it is going to be at least a hundred times
             | slower than an hand rolled implementation.
             | 
             | Also, although not likely to be removed anytime soon from
             | existing systems, POSIX has declared the context API
             | obsolescent a while ago (it might actually no longer be
             | part of the standard).
        
               | cyberax wrote:
               | Signal mask? What century are we in?
               | 
               | It can be safely ignored for the vast majority of apps.
               | If you're using multithreading (quite likely if you're
               | doing coroutines), then signals are not a good fit
               | anyway.
        
               | gpderetta wrote:
               | Aside from the fact that the signal mask is still
               | relevant in 2026 and even for multithreaded programs,
               | that doesn't have anything to do with the fact that POSIX
               | requires swapcontext to preserve it.
        
         | Joker_vD wrote:
         | Hmm. I'm _fairly_ certain that most of that assembly code for
         | saving /restoring registers can be replaced with
         | setjmp/longjmp, and only control transfer itself would require
         | actual assembly. But maybe not.
         | 
         | That's the problem with register machines, I guess.
         | Interestingly enough, BCPL, its main implementation being a
         | p-code interpreter of sorts, has pretty trivially supported
         | coroutines in its "standard" library since the late seventies
         | -- as you say, all you need to save is the current stack
         | pointer and the code pointer.
        
           | lelanthran wrote:
           | > Hmm. I'm fairly certain that most of that assembly code for
           | saving/restoring registers can be replaced with
           | setjmp/longjmp, and only control transfer itself would
           | require actual assembly.
           | 
           | Actually you don't even need setjmp/longjmp. I've used a
           | library (embedded environment) called protothreads (plain C)
           | that abused the preprocessor to implement stackful
           | coroutines.
           | 
           | (Defined a macro that used the __LINE__ macro coupled with
           | another macro that used a switch statement to ensure that
           | calling the function again made it resume from where the last
           | YIELD macro was encountered)
        
             | Cloudef wrote:
             | Wouldnt that be stackless (shared stack)
        
               | lelanthran wrote:
               | Correct; stackless. I misspoke.
        
           | gpderetta wrote:
           | setjmp + longjump + sigaltstack is indeed the old trick.
        
           | zabzonk wrote:
           | You can do a lot of horrible things with setjmp and friends.
           | I actually implemented some exception throw/catch macros
           | using them (which did work) for a compiler that didn't
           | support real C++ exceptions. Thank god we never used them in
           | production code.
           | 
           | This would be about 32 years ago - I don't like thinking
           | about that ...
        
             | gpderetta wrote:
             | GCC still uses sj/lj by default on some targets to
             | implement exceptions.
        
         | Sharlin wrote:
         | C++ destructors and exception safety will likely wreak havoc
         | with any "simple" assembly/longjmp-based solution, unless
         | severely constraining what types you can use within the
         | coroutines.
        
           | fluoridation wrote:
           | Not really. I've done it years ago. The one restriction for
           | code inside the coroutine is that it mustn't catch (...). You
           | solve destruction by distinguishing whether a couroutine is
           | paused in the middle of execution or if it finished running.
           | When the coroutine is about to be destructed you run it one
           | last time and throw a special exception, triggering
           | destruction of all RAII objects, which you catch at the
           | coroutine entry point.
           | 
           | Passing uncaught exceptions from the coroutine up to the
           | caller is also pretty easy, because it's all synchronous. You
           | just need to wrap it so it can safely travel across the gap.
           | You can restrict the exception types however you want. I
           | chose to support only subclasses of std::exception and handle
           | anything else as an unknown exception.
        
             | Sharlin wrote:
             | Thanks, that's interesting.
        
             | pjc50 wrote:
             | > Passing uncaught exceptions from the coroutine up to the
             | caller is also pretty easy, because it's all synchronous.
             | You just need to wrap it so it can safely travel across the
             | gap
             | 
             | This is also how dotnet handles it, and you can choose
             | whether to rethrow at the caller site, inspect the
             | exception manually, or run a continuation on exception.
        
             | gpderetta wrote:
             | > mustn't catch (...)
             | 
             | You could use the same trick used by glibc to implement
             | unstoppable exceptions for POSIX cancellation: the
             | exception rethrows itself from its destructor.
        
         | MisterTea wrote:
         | A much nicer code base to study is: https://swtch.com/libtask/
         | 
         | The stack save/restore happens in:
         | https://swtch.com/libtask/asm.S
        
         | socalgal2 wrote:
         | As an x-gamedev, suspect/resume/stackful coroutines made them
         | too heavy to have several thousand of them running during a
         | game loop for our game. At the time we used GameMonkey Script:
         | https://github.com/publicrepo/gmscript
         | 
         | That was over 20 years ago. No idea what the current hotness
         | is.
        
           | fluoridation wrote:
           | Several thousand? What were you using them for? Coroutines'
           | main utility is that they let you write complex code that
           | pauses and still looks sensible, so for games, you'd
           | typically put stuff like the behavior of an NPC in a
           | coroutine. If you have thousands of things to put each in its
           | own coroutine, they must have been really, _really_ simple
           | stuff. At that point, the cost of context switching can
           | become significant.
        
         | TuxSH wrote:
         | > every async "function call" heap allocates.
         | 
         | > require the STL
         | 
         | That it _has to_ heap-allocate if non-inlined is a
         | misconception. This is only the default behavior.
         | 
         | One can define:
         | 
         | void *operator new(size_t sz, Foo &foo)
         | 
         | in the coro's promise type, and this:
         | 
         | - removes the implicitly-defined operator new
         | 
         | - forces the coro's signature to be CoroType f(Foo &foo), and
         | forwards arguments to the "operator new" one defined
         | 
         | Therefore, it's pretty trivial to support coroutines even when
         | heap cannot be used, especially in the non-recursive case.
         | 
         | Yes, green threads ("stackful coroutines") are more
         | straightforward to use, however:
         | 
         | - they can't be arbitrarily destroyed when suspended (this
         | would require stack unwinding support and/or active support
         | from the green thread runtime)
         | 
         | - they are very ABI dependent. Among the "few registers" one
         | has to save FPU registers. Which, in the case of older Arm
         | architectures, and codegen options similar to -mgeneral-regs-
         | only (for code that runs "below" userspace). Said FPU registers
         | also take a lot of space in the stack frame, too
         | 
         | Really, stackless coros are just FSM generators (which is
         | obvious if one looks at disasm)
        
           | gpderetta wrote:
           | A stackful coroutine implementation has to save exactly the
           | same registers that a stackless one has to: the live ones at
           | the suspension point.
           | 
           | A pure library implementation that uses on normal function
           | call semantics obviously needs to conservatively save at
           | least all callee-save registers, but that's not the only
           | possible implementation. An implementation with compiler help
           | should be able to do significantly better.
           | 
           | Ideally the compiler would provide a built-in, but even, for
           | example, an implementation using GCC inline ASM with proper
           | clobbers can do significantly better.
        
       | bradrn wrote:
       | In Haskell this technique has been called 'reinversion of
       | control': http://blog.sigfpe.com/2011/10/quick-and-dirty-
       | reinversion-o...
        
       | twoodfin wrote:
       | As the author lays out, the thing that made coroutines click for
       | me was the isomorphism with state machine-driven control flow.
       | 
       | That's similar to most of what makes C++ tick: There's no deep
       | magic, it's "just" type-checked syntactic sugar for code patterns
       | you could already implement in C.
       | 
       | (Occurs to me that the exceptions to this ... like exceptions,
       | overloads, and context-dependent lookup ... are where C++ has
       | struggled to manage its own complexity.)
        
         | HarHarVeryFunny wrote:
         | If you need to implement an async state machine, couldn't that
         | just as easily be done with std::future? How do coroutines make
         | this cleaner/better?
        
           | LatencyKills wrote:
           | std::future doesn't give you a state machine. You get the
           | building blocks you have to assemble into one manually.
           | Coroutines give you the same building blocks but let the
           | compiler do the assembly, making the suspension points
           | visible in the source while hiding the mechanical
           | boilerplate.
           | 
           | This is why coroutine-based frameworks (e.g., C++20
           | coroutines with cppcoro) have largely superseded future-
           | chaining for async state machine work -- the generated code
           | is often equivalent, but the source code is dramatically
           | cleaner and closer to the synchronous equivalent.
           | 
           | (me: ex-Visual Studio dev who worked extensively on our C++
           | coroutine implementation)
        
             | physPop wrote:
             | I feel like thats really oversellign coro -- theres still a
             | TON of boilerplate
        
               | LatencyKills wrote:
               | My response specifically addressed the question of _why_
               | you might choose one option over the other.
               | 
               | Do you believe that std::future is the better option?
        
             | HarHarVeryFunny wrote:
             | It doesn't seem like a clear win to me. The only "assembly"
             | required with std::future is creating the associated
             | promise and using it to signal when that async step is
             | done, and the upside is a nice readable linear flow, as
             | well as ease of integration (just create a thread to run
             | the state machine function if want multiple in parallel).
             | 
             | With the coroutine approach using yield, doesn't that mean
             | the caller needs to decide when to call it again? With the
             | std::future approach where it's event driven by the promise
             | being set when that state/step has completed.
        
               | LatencyKills wrote:
               | You are describing a single async step, not a state
               | machine. "Create a promise, set it when done", that's one
               | state. A real async state machine has N states with
               | transitions, branching, error handling, and cleanup
               | between them.
               | 
               | > "The only 'assembly' required is creating the
               | associated promise"
               | 
               | Again, that is only true for one step. For a state
               | machine with N states you need explicit state enums or a
               | long chain of .then() continuations. You also need to the
               | manage the shared state across continuations (normally on
               | the heap). You need to manage manual error propagation
               | across each boundary and handle the cancellation tokens.
               | 
               | You only get a "A nice readable linear flow" using
               | std:future when 1) using a blocking .get() on a thread,
               | or 2) .then() chaining, which isn't "nice" by any means.
               | 
               | Lastly, you seem to be conflating a co_yield (generator,
               | pull-based) with co_await (event-driven, push-based).
               | With co_await, the coroutine is resumed by whoever
               | completes the awaitable.
               | 
               | But what do I know... I only worked on implementing
               | coroutines in cl.exe for 4 years. ;-)
        
               | HarHarVeryFunny wrote:
               | I only mentioned co_yield() since that's what the article
               | was (ab)using, although perhaps justifiably so. It seems
               | the coroutine support was added to C++ in a very flexible
               | way, but so low level as to be daunting/inconvenient to
               | use. It needs to have more high level facilities (like
               | Generators) built on top.
               | 
               | What I was thinking of as a state machine with using
               | std::future was a single function state machine, using
               | switch (state) to the state specific dispatch of asynch
               | ops using std::future, wait for completion then select
               | next state.
        
               | LatencyKills wrote:
               | > as to be daunting/inconvenient to use
               | 
               | I don't even know how to respond to that. How in the
               | world are you using C++ professionally if you think
               | coroutines are "daunting"? No one uses C++ for it's
               | "convenience" factor. We use it for the power and control
               | it affords.
               | 
               | > What I was thinking of as a state machine with using
               | std::future was a single function state machine, using
               | switch (state) to the state specific dispatch of asynch
               | ops using std::future, wait for completion then select
               | next state.
               | 
               | Uh huh. What about error propagation and all the other
               | _very real_ issues I mentioned that you are just
               | ignoring? Why not just let the compiler do all the work
               | the way it was spec 'ed and implemented?
        
       | mgaunard wrote:
       | Coroutines is just a way to write continuations in an imperative
       | style and with more overhead.
       | 
       | I never understood the value. Just use lambdas/callbacks.
        
         | affenape wrote:
         | Not necessarily. A coroutine encapsulates the entire state
         | machine, which might pe a PITA to implement otherwise. Say, if
         | I have a stateful network connection, that requires
         | initialization and periodic encryption secret renewal, a
         | coroutine implementation would be much slimmer than that of a
         | state machine with explicit states.
        
         | usrnm wrote:
         | > Just use lambdas/callbacks
         | 
         | "Just" is doing a lot of work there. I've use callback-based
         | async frameworks in C++ in the past, and it turns into pure
         | hell very fast. Async programming is, basically, state machines
         | all the way down, and doing it explicitly is not nice. And
         | trying to debug the damn thing is a miserable experience
        
           | mgaunard wrote:
           | You can embed the state in your lambda context, it really
           | isn't as difficult as what people claim.
           | 
           | The author just chose to write it as a state machine, but you
           | don't have to. Write it in whatever style helps you reach
           | correctness.
        
             | Sharlin wrote:
             | You still need the state and the dispatcher, even if the
             | former is a little more hidden in the implicit closure
             | type.
        
         | Sharlin wrote:
         | Did you read the article? As the author says, it becomes a
         | state machine hell very quickly beyond very simple examples.
        
           | kccqzy wrote:
           | I just don't agree that it always becomes a state machine
           | hell. I even did this in C++03 code before lambdas. And
           | honestly, because it was easy to write careless spaghetti
           | code, it required a lot more upfront thought into code
           | organization than just creating lambdas willy-nilly. The
           | resulting code is verbose, but then again C++ itself is a
           | fairly verbose language.
        
         | DonHopkins wrote:
         | The Unity editor does not let you examine the state hidden in
         | your closures or coroutines. (And the Mono debugger is a
         | steaming pile of shit.)
         | 
         | Just put your state in visible instance variables of your
         | objects, and then you will actually be able to see and even
         | edit what state your program is in. Stop doing things that make
         | debugging difficult and frustratingly opaque.
        
           | jayd16 wrote:
           | Use Rider or Visual Studio. Debugging coroutines should be
           | easy. You just can't step over any yield points so you need
           | to break after execution is resumed. It's mildly tedious but
           | far from impossible.
        
         | spacechild1 wrote:
         | > Just use lambdas/callbacks.
         | 
         | Lol, no thanks. People are using coroutines exactly to avoid
         | callback hell. I have rewritten my own C++ ASIO networking code
         | from callback to coroutines (asio::awaitable) and the
         | difference is night and day!
        
         | jayd16 wrote:
         | You can structure coroutines with a context so the runtime has
         | an idea when it can drop them or cancel them. Really nice if
         | you have things like game objects with their own lifecycles.
         | 
         | For simple callback hell, not so much.
        
         | duped wrote:
         | The value is fewer indirect function calls heap allocations (so
         | less overhead than callbacks) and well defined tasks that you
         | can select/join/cancel.
        
         | socalgal2 wrote:
         | I'll take the bait. Here's a coroutine
         | waitFrames(5); // wait 5 frames         fireProjectile();
         | waitFrames(15);         turnLeft(-30/*deg*/, 120); // turn left
         | over 120 frames         waitFrames(10);
         | fireProjectile();         // spin and shoot         for (i of
         | range(0, 360, 60)) {           turnRight(60, 90);  // turn 60
         | degrees over 90 frames           fireProjectile();         }
         | 
         | 10 lines and I get behavior over time. What would your non-
         | coroutine solution look like?
        
           | mgaunard wrote:
           | Given a coroutine body
           | 
           | ``` int f() { a; co_yield r; b; co_return r2; } ```
           | 
           | this transforms into
           | 
           | ``` auto f(auto then) { a; return then(r, [&]() { b; return
           | then(r2); }); }; ```
           | 
           | You can easily extend this to arbitrarily complex statements.
           | The main thing is that obviously, you have to worry about the
           | capture lifetime yourself (coroutines allocate a frame
           | separate from the stack), and the syntax causes nesting for
           | every statement (but you can avoid that using operator
           | overloading, like C++26/29 does for executors)
        
             | spacechild1 wrote:
             | How is this better than the equivalent coroutine code? I
             | don't see any upsides from a user's perspective.
             | 
             | > The main thing is that obviously, you have to worry about
             | the capture lifetime yourself
             | 
             | This is a _big_ deal! The fact that the coroutine frame is
             | kept alive and your state can just stay in local variables
             | is one of the main selling points. I experienced this
             | first-hand when I rewrote callback-style C++ ASIO code to
             | the new coroutine style. No more [self=shared_from_this()]
             | and other shenanigans!
        
       | momocowcow wrote:
       | No serious devs even uses Unity coroutines. Terrible control flow
       | and perf. Fine for small projects on PC.
        
         | kdheiwns wrote:
         | In all of my years of professional game dev, I can verify that
         | this is not even remotely true. They're used basically
         | everywhere. They're very common when you need something to
         | update for a set period of time but managing the state outside
         | a very local context would just make the code a mess.
         | 
         | Unity's own documentation for changing scenes uses coroutines
        
         | krajzeg wrote:
         | Echoing the thoughts of the only current sibling comment: lots
         | of "serious" developers (way to gatekeep here) definitely use
         | coroutines, when they make sense. As mentioned, it's one of the
         | best ways to have something update each frame for a short
         | period of time, then neatly go away when it's not needed
         | anymore. Very often, the tiny performance hit you take is
         | completely outweighed by the maintanability/convenience.
        
           | DonHopkins wrote:
           | ...and then crash when any object it was using gets deleted
           | while it's still running, like when the game changes scenes,
           | but it becomes a manual, error-prone process to track down
           | and stop all the coroutines holding on to references, that
           | costs much more effort than it saves.
           | 
           | I've been a serious Unity developer for 16 years, and I avoid
           | coroutines like the plague, just like other architectural
           | mistakes like stringly typed SendMessage, or UnityScript.
           | 
           | Unity coroutines are a huge pain in the ass, and a lazy
           | undisciplined way to do things that are easy to do without
           | them, using conventional portable programming techniques that
           | make it possible to prevent edge conditions where things fall
           | through the cracks and get forgotten, where references
           | outlive the objects they depend on ("fire-and-forget" gatling
           | foot-guns).
           | 
           | Coroutines are great -- right up until they aren't.
           | 
           | They give you "nice linear code" by quietly turning control
           | flow into a distributed state machine you no longer control.
           | Then the object gets destroyed, the coroutine keeps running,
           | and now you're debugging a null ref 200 frames later in a
           | different scene with an obfuscated call stack and no
           | ownership.
           | 
           | "Just stop your coroutines" sounds good until you realize
           | there's no coherent ownership model. Who owns it? The
           | MonoBehaviour? The caller? The scene? Every object it has a
           | reference to? The thing it captured three yields ago? The
           | cure is so much worse than the disease.
           | 
           | Meanwhile: No static guarantees about lifetime. No structured
           | cancellation. Hidden allocation/GC from yield instructions.
           | Execution split across frames with implicit state you can't
           | inspect.
           | 
           | Unity has a wonderful editor that lets you inspect and edit
           | the state of the entire world: EXCEPT FOR COROUTINES! If you
           | put your state into an object instead of local variables in a
           | coroutine, you can actually see the state in the editor.
           | 
           | All of this to avoid writing a small explicit state machine
           | or update loop -- Unity ALREADY has Update and FixedUpdate
           | just for that: use those.
           | 
           | Coroutines aren't "cleaner" -- they just defer the mess until
           | it's harder to reason about.
           | 
           | If you can't handle state machines, then you're even less
           | equipped to handle coroutines.
        
             | bob1029 wrote:
             | > Who owns it? The MonoBehaviour? The caller? The thing it
             | captured three yields ago?
             | 
             | The monobehavior that invoked the routine owns it and is
             | capable of cancelling it at typical lifecycle boundaries.
             | 
             | This is not a hill I would die on. There's a lot of other
             | battles to fight when shipping a game.
        
               | DonHopkins wrote:
               | And then you're bending over backwards and have made so
               | much more busy work for yourself than you would have if
               | you'd just done it the normal way, in which all your
               | state would be explicitly visible and auditable in the
               | editor.
               | 
               | The biggest reason for using Unity is its editor. Don't
               | do things that make the editor useless, and are invisible
               | to it.
               | 
               | The problem with coroutines is that they generate
               | invisible errors you end up shipping and fighting long
               | after you shipped your game, because they're so hard to
               | track down and reproduce and diagnose.
               | 
               | Sure you can push out fixes and updates on Steam, but how
               | about shipping games that don't crash mysteriously and
               | unpredictably in the first place?
        
             | Arch485 wrote:
             | I dunno, I've worked on some pretty big projects that have
             | used lots of coroutines, and it's pretty easy to avoid all
             | of the footguns.
             | 
             | I'm not advocating for the ubiquitous use of coroutines
             | (there's a time and place), but they're like anything else:
             | if you don't know what you're doing, you'll misuse them and
             | cause problems. If you RTFM and understand how they work,
             | you won't have any issues.
        
               | DonHopkins wrote:
               | They're a crutch for people who don't know what they're
               | doing, so of course they invite a whole host of problems
               | that are harder to solve than doing it right in the first
               | place.
               | 
               | If you strictly require people to know exactly what
               | they're doing and always RTFM and perfectly understand
               | how everything works, then they already know well enough
               | to avoid coroutines and SendMessage and UnityEvents and
               | other footguns in the first place.
               | 
               | It's much easier and more efficient to avoid all of the
               | footguns when you simply don't use any of the footguns.
        
             | kdheiwns wrote:
             | Never had a crash from that. When the GameObject is
             | destroyed, the coroutine is gone. If you're using a
             | coroutine to manage something outside the scope of the
             | GameObject itself, that's a problem with your own design,
             | not the coroutine itself.
             | 
             | It'd be like complaining about arrays being bad because if
             | you pass a pointer to another object, nuke the original
             | array, then try to access the data, it'll cause an error.
             | That's kind of... your own fault? Got to manage your data
             | better.
             | 
             | Unity's own developers use them for engine code. To claim
             | it's just something for noobs is a bit of an interesting
             | take, since, well, the engine developers are clearly using
             | them and I doubt they're Unity noobs. They made the engine.
        
             | jayd16 wrote:
             | So if you need to conditionally tick something or you want
             | to wait for an effect to finish, etc., you're using
             | Update() with if() statements?
             | 
             | The same code in a coroutine hits the same lifecycle
             | failures as Update() anyway. You don't gain any safety by
             | moving it to Update().
             | 
             | > No structured cancellation.
             | 
             | Call StopCoroutine with the Coroutine object returned by
             | StartCoroutine. Of course you can just pass around a
             | cancellation token type thing as well.
             | 
             | > Hidden allocation/GC from yield instructions.
             | 
             | Hidden how? You're calling `new` or you're not.
             | 
             | Instead of fighting them, you should just learn how to use
             | coroutines. They're a lot nicer than complicated logic in
             | Update().
        
         | voidUpdate wrote:
         | Just out of interest, how many serious unity devs have you
         | talked to?
        
           | DonHopkins wrote:
           | I've talked to some non-serious unity devs, like Peter
           | Molyneux...
           | 
           | https://news.ycombinator.com/item?id=47110605
           | 
           | >1h 48m 06s, with arms spread out like Jesus H Christ on a
           | crucifix: "Because we can dynamically put on ANY surface of
           | the cube ANY image we like. So THAT's how we're going to
           | surprise the world, is by giving clues about what's in the
           | middle later on."
           | 
           | https://youtu.be/24AY4fJ66xA?t=6486
           | 
           | Click. Click. Click. Click. Click. Click. Click. Click.
           | Click. Click. Click. Click. Click. Click. Click. Click.
           | Click. Click. Click. Click. Click. Moo!
           | 
           | https://www.gamedeveloper.com/design/the-i-curiosity-i-
           | exper...
           | 
           | >"I'm jealous that [Molyneux] made a more boring clicking
           | game than I did." -Ian Bogost
           | 
           | >"I also think Curiosity was brilliant and inspired. But that
           | doesn't make it any less selfish or brazen. Curiosity was
           | _not_ an experiment.  'Experiment' is a rhetorical ruse meant
           | to distract you from the fact that it's promotional." -Ian
           | Bogost
        
             | mjr00 wrote:
             | Molyneux is obviously a well-known gamedev figure, but he's
             | always been much more on the design side than programming
             | side, as opposed to someone like Carmack or even J Blow. I
             | wouldn't take his opinions on minutiae like coroutines as
             | authoritative.
        
       | pjmlp wrote:
       | As I mentioned on the Reddit thread,
       | 
       | This is quite understandable when you know the history behind how
       | C++ coroutines came to be.
       | 
       | They were initially proposed by Microsoft, based on a C++/CX
       | extension, that was inspired by .NET async/await implementation,
       | as the WinRT runtime was designed to only support asynchronous
       | code.
       | 
       | Thus if one knows how the .NET compiler and runtime magic works,
       | including custom awaitable types, there will be some common
       | bridges to how C++ co-routines ended up looking like.
        
       | bullen wrote:
       | Coroutines generally imply some sort of magic to me.
       | 
       | I would just go straight to tbb and concurrent_unordered_map!
       | 
       | The challenge of parallelism does not come from how to make
       | things parallel, but how you share memory:
       | 
       | How you avoid cache misses, make sure threads don't trample each
       | other and design the higher level abstraction so that all layers
       | can benefit from the performance without suffering turnaround
       | problems.
       | 
       | My challenge right now is how do I make the JVM fast on native
       | memory:
       | 
       | 1) Rewrite my own JVM. 2) Use the buffer and offset structure
       | Oracle still has but has deprecated and is encouraging people to
       | not use.
       | 
       | We need Java/C# (already has it but is terrible to write
       | native/VM code for?) with bottlenecks at native performance and
       | one way or the other somebody is going to have to write it?
        
         | pjc50 wrote:
         | > C# (already has it but is terrible to write native/VM code
         | for?)
         | 
         | What do you mean here? Do you mean hand-writing MSIL or native
         | interop (pinvoke) or something else?
        
           | bullen wrote:
           | No I meant this but for C# is a whole lot more complex:
           | 
           | http://move.rupy.se/file/jvm.txt
        
         | themafia wrote:
         | > some sort of magic to me.
         | 
         | Your stack is on the heap and it contains an instruction
         | pointer to jump to for resume.
        
       | wiseowise wrote:
       | Looking at C++ made me understand the point of Rust.
        
       | sagebird wrote:
       | >> To misquote Kennedy, "we chose to focus coroutines on
       | generator in C++23, not because it is hard, but because it is
       | easy".
       | 
       | Appreciate this humor -- absurd, tasteful.
        
       | FpUser wrote:
       | I do not find so called "green threads" useful at all. In my
       | opinion except some very esoteric cases they serve no purpose in
       | "native" languages that have full access to all OS threading and
       | IO facilities. Useful only in "deficient" environments like
       | inherently single threaded request handlers like NodeJS.
        
       | nice_byte wrote:
       | I don't know, I'm not convinced with this argument.
       | 
       | The "ugly" version with the switch seems much preferable to me.
       | It's simple, works, has way less moving parts and does not
       | require complex machinery to be built into the language. I'm open
       | to being convinced otherwise but as it stands I'm not seeing any
       | horrible problems with it.
        
       ___________________________________________________________________
       (page generated 2026-03-25 23:01 UTC)