7 ms·
Looking at Unity made me understand the point of C++ coroutines
- abcde666777 7mo agoMore 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 7mo ago> 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 7mo agoYou're in luck - it's the first talk at this link, "The Polling Problem": https://www.gdcvault.com/play/1018040/Architecture-Tricks-Managing-Behaviors-in 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!
- truepricehq 7mo ago[dead]
- syncurrent 7mo agoThis timing additions to a language is also at the core of imperative synchronous programming languages like Este rel, Céu or Blech.
- manoDev 7mo agoThis 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 7mo ago> 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 7mo agoNot 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 7mo agoASIO is also available outside of boost! https://github.com/chriskohlhoff/asio https://github.com/chriskohlhoff/asio
- lionkor 7mo agoFor anyone wondering; this isn't a hack, that's the same library, just as good, just without boost dependencies.
- spacechild1 7mo agoThanks 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 7mo agoMuch 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 7mo ago> 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)
- pjc50 7mo agoAlways 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 7mo agoNot too different from C++'s iterator interface for generators, I guess.
- debugnik 7mo agoNot 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 7mo agoUnity 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/ScriptReference/Awaitable.html https://docs.unity3d.com/6000.3/Documentation/ScriptReferenc...
- debugnik 7mo agoOh 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 7mo agoIt'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.
- Joker_vD 7mo agoSimon 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.chiark.greenend.org.uk/~sgtatham/quasiblog/coroutines-c++20/ https://web.archive.org/web/20260105235513/https://www.chiar...
- zozbot234 7mo agoPeople love to complain about Rust async-await being too complicated, but somehow C++ manages to be even worse. C++ never disappoints!
- deleted 7mo ago[deleted]
- 01HNNWZ0MV43FF 7mo agoasync 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 7mo agoLanguages like Swift do manage to make it much simpler. The culture guiding Rust design pretty clearly treats complexity as a goal.
- quietbritishjim 7mo agoUsing 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-go-statement-considered-harmful/ https://vorpus.org/blog/notes-on-structured-concurrency-or-g... [2] https://trio.readthedocs.io/en/stable/ https://trio.readthedocs.io/en/stable/
- nananana9 7mo agoYou 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 https://github.com/edubart/minicoro
- pjc50 7mo ago> 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 7mo agoBoost has stackful coroutines. They also used to be in posix (makecontext).
- fluoridation 7mo agoI 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 7mo agoThere 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.
- maltyxxx 7mo ago[flagged]
- bradrn 7mo agoIn Haskell this technique has been called ‘reinversion of control’: http://blog.sigfpe.com/2011/10/quick-and-dirty-reinversion-of-control.html http://blog.sigfpe.com/2011/10/quick-and-dirty-reinversion-o...
- rando-guy 7mo ago[dead]
- twoodfin 7mo agoAs 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 7mo agoIf 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 7mo agostd::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 7mo agoI feel like thats really oversellign coro -- theres still a TON of boilerplate
- LatencyKills 7mo agoMy 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?
- 7mo ago
- mgaunard 7mo agoCoroutines 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 7mo agoNot 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 7mo ago> 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 7mo agoYou 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 7mo agoYou still need the state and the dispatcher, even if the former is a little more hidden in the implicit closure type.
- Sharlin 7mo agoDid you read the article? As the author says, it becomes a state machine hell very quickly beyond very simple examples.
- momocowcow 7mo agoNo serious devs even uses Unity coroutines. Terrible control flow and perf. Fine for small projects on PC.
- kdheiwns 7mo agoIn 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 7mo agoEchoing 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 7mo ago...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.
- pjmlp 7mo agoAs 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 7mo agoCoroutines 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 7mo ago> 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 7mo agoNo I meant this but for C# is a whole lot more complex: http://move.rupy.se/file/jvm.txt http://move.rupy.se/file/jvm.txt
- themafia 7mo ago> some sort of magic to me. Your stack is on the heap and it contains an instruction pointer to jump to for resume.
- wiseowise 7mo agoLooking at C++ made me understand the point of Rust.
- sagebird 7mo ago>> 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 7mo agoI 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.
- bigstrat2003 7mo agoYeah I agree that user threads are overused by programmers. For most situations, using an OS thread is going to be far easier to work with. People like to cite the drawback that context switching overhead becomes a problem when you have thousands of threads, but the reality is that most people are not writing software that needs to handle many thousands of users all at once. Using green threads to handle such large scale instead of OS threads is a prime example of YAGNI.
- tliltocatl 7mo agoBesides context switching, another issue with OS threads is that you need to carefully use synchronization primitives and watch for missing barriers. Green threads are a bit more forgiving.
- FpUser 7mo agoMain point of real threads is that the are executed simultaneously on different cores (single core can of course still run few threads if needed), not by cooperative switching. Hence you need sync primitives so better get used to it. Having few threads on real cores can dramatically increase performance. Green threads is just syntactic sugar around need to process multiple requests from a single real thread without blocking on io calls. It is somewhat helpful but it is a way around, not a generic recipe for performance increase and scalability
- tliltocatl 7mo ago> Main point of real threads is that the are executed simultaneously on different cores Sorry, that's about as far from truth as you can get. Threads were a thing way before multicore was available. Windows got them since Win95/NT4.0, and these didn't support multicore, nor did most of OpenVMS machines where NT thread model originated run multicore (were there any multicore VAXen at all). The two main points for threads back then was to simplify concurrent IO and to be able to run a background computation without hanging the UI. Unix was different because it was initially oriented towards timesharing aka. running a single program for user, so threads were seen as redundant complexity (as were async IO - so even today "I want to run something yet be able get more user input at any time" can sometimes be a PITA). And year, not everything needs scalability. Scalability is actually actively harmful for everything that's not a server. E. g. UI needs to be responsive - but that doesn't necessary mean "run on multiple cores", rather "being able to preempt low-priority stuff". And usually you don't want to deal with multicore synchronization. MCUs rarely have multiple cores. Yet multi-threading is ubiquitous, and on most RTOSes threads are much closer to "green threads" than Windows ones. Through as I keep screaming in every coroutine thread, I really wish C stackless coroutines were a thing, you can't afford a stack for everything. There are more to this world than the boring AWS crud.
- nice_byte 7mo agoI 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.
- nulltrace 7mo agoSwitch is fine until you hit five or six states with cleanup in each branch. Then it's just a worse version of what coroutines give you for free.
- BSTRhino 7mo agoThis is one reason why I built coroutines into my game programming language Easel (https://easel.games https://easel.games). I think they let you keep the flow of the code matching the flow of the your logic (top-to-bottom), rather than jumping around, and so I think they are a great tool for high-level programming. The main thing is stopping the coroutines when the entity dies, and in Easel that is done by implying ownership from the context they are created in. It is quite a cool way of coding I think, avoids the state machines like the OP stated, keeps everything straightforward step-by-step and so all the code feels more natural in my opinion. In Easel they are called behaviors if anyone is interested in more detail: https://easel.games/docs/learn/language/behaviors https://easel.games/docs/learn/language/behaviors
- Animats 7mo agoMost game engines seem to have some coroutine kludge.
- nottorp 7mo ago> turns it into some sort of ugly state machine Why are people afraid of state machines? There's been sooo much effort spent on hiding them from the programmer...
- matheusmoreira 7mo agoThey're essentially callable, stateful, structured gotos. Difficult to understand for the uninitiated. For example, generators. Also known as semicoroutines. https://langdev.stackexchange.com/a/834 https://langdev.stackexchange.com/a/834 This: generator fib() { a, b = 1, 2 while (a<100) { b, a = a, a+b yield a } yield a-1 } Becomes this: struct fibState { a, b, position } int fib(fibState state) { switch (fibState.postion) { case 0: fibState.a, fibState.b = 1,2 while (a<100) { fibState.b, fibState.a = fibState.a, fibState.a+fibState.b // switching the context fibState.position = 1; return fibState.a; case 1: } fibState.position = 2; return fibState.a-1 case 2: fibState.position = -1; } } The ugly state machine example presented in the article is also a manual implementation of a generator. It's as palatable to the normal programmer as raw compiler output. Being written in C++ makes it even uglier and more complicated. The programming language I made is a concrete example of what programming these things manually is like. I had to write every primitive as a state machine just like the one above. https://www.matheusmoreira.com/articles/delimited-continuations-in-lone-lisp https://www.matheusmoreira.com/articles/delimited-continuati...
- nottorp 7mo agoWhat you've given is an example of how to implement a coroutine though. Not of how to write a state machine based application without hiding the state machine behind abstractions.
- tliltocatl 7mo agoBecause they are unstructured and non-modular. And yes, graphical notation sucks.
- appstorelottery 7mo agoI've been doing a lot of work with ECS/Dots recently and once I wrapped my head around it - amazing. I recall working on a few VR projects - where it's imperative that you keep that framerate solid or risk making the user physically sick - this is where really began using coroutines for instantiating large volumes of objects and so on (and avoiding framerate stutter). ECS/Dots & the burst compiler makes all of this unnecessary and the performance is nothing short of incredible.
- djmips 7mo agoThe 'primitive' SCUMM language used for writing Adventure Games like Maniac Mansion had coroutines - an ill fated attempt to convert to using Python was hampered by Python (at the time) having no support for yield.
- sta1n 7mo ago[dead]
- tliltocatl 7mo agoStackless coroutines in C when? As an embedded dev, I miss them deeply. Certainly not enough RAM to give a separate stack for everything and rewriting every async call as a callback sequence sucks.