10 ms·
My tutorial and take on C++20 coroutines
- creedor 6y agoI'm a bit confused by all of this. The idea of coroutines existed since the 60's. Then we came up with OOP that tries to combine function + data. And now we abolished this, are back at only functions and are now solving the 'state capture' problem for functions again with: Coroutines? How does the history of programming paradigms/patterns make any sense? :) Good article though.
- sheenobu 6y agoThe glib answer is that these kinds of things are cyclical. But C++ coroutines don't seem too incompatible with the level of OOP you already get in C++, no?
- Mikhail_Edoshin 6y agoObjects and coroutines are very closely related.
- jussij 6y agoThese are features built into the language designed to make multi-threaded code safer and easier to write.
- OskarS 6y agoYour analysis of history there is a bit lacking. Coroutines didn't go out of fashion because of OOP, it went out of fashion because of structured programming and higher level languages. Coroutines are doable if you're programming directly in assembly, but if you want to do it in C-like structured languages, it turns out that it's really tricky: the whole concept of those languages are about having a single stack and hierarchical lexical structure. You can do coroutines in languages like this, but unless you want to do nasty things with longjmp and manually manipulating the stack pointer, they simply aren't built for it. You can build structured languages with first-class support for constructs with coroutines, but it took a couple of decades of programming language development for that to happen. Even today, most of the languages that support coroutines (C++20 included), only has support for the stackless kind. Basically the only languages with full stackful coroutine support in wide use are Lua and (sort-of) Go.
- spc476 6y agoOr take advantage of the ABI of the runtime, and use assembly. [1] Yeah, not portable. But using setjmp()/longjmp() has issues as well (way too easy to mess it up because it doesn't do what you think it's doing). [1] https://github.com/spc476/C-Coroutines https://github.com/spc476/C-Coroutines
- gpderetta 6y agoThere is no particular difficulty in having one-shot continuations in C (or C++) and in fact over the last few decades there have been plenty of libraries implementing them. They just never caught on with the general C and C++ programming population, although they were popular in some niches. C with Classes had them from the beginning, as Stroustrup liked them in Simula, but then (like many other things) had to take them out of the language after user feedback. Re stackless coroutines and language support, while it is relatively straightforward to have stackfull coroutines as a library, the stackless kind in practice needs to be implemented as a language feature.
- diskmuncher 6y ago> Basically the only languages with full stackful coroutine support in wide use are Lua and (sort-of) Go. Aren't Javascript Generators also coroutines?
- gpderetta 6y agothey are, as far as I know, stackless. I.e. you can only yield from the generator top level frame.
- jcelerier 6y ago> . And now we abolished this, ... no. coroutines are not supposed to be used in, like, more than an extremely small fraction of your codebase if you want to keep your code understandable
- oivey 6y agoEh, I think coroutines are a convenient way to achieve concurrency and parallelism. If you limit mutation and try to reduce the long reaching accessibility of variables then I think they’re generally very understandable, especially compared to other concurrency/parallelism paradigms.
- jcelerier 6y ago> Eh, I think coroutines are a convenient way to achieve concurrency and parallelism. but how often do you need concurrency and parallelism outside of handling network requests and performing some complicated math algorithm (which may be in a lib that you don't touch anyways, e.g. FFT>) ? e.g. most UI code has to run on the main thread due to macOS / Windows limitations, and in most UI software it is extremely rare to have the kind of chains of callbacks whose readability gets improved by coroutines.
- gpderetta 6y agoCoroutines are not really for parallelism. I doubt you'll see them much around math heavy code. Possibly if someone implements some Cilk style work stealing executor...
- pjmlp 6y agoIdeally, but they taint everything they touch, that is why in C# I always start them from some point with Task.Run(), or leave it for the event and request handlers.
- deleted 6y ago[deleted]
- panpanna 6y agoIs learning modern C++ worth the effort in 2021? I know classic c++, but don't really use it anymore.
- brobdingnagians 6y agoDepends on what you want to use it for. Game programming with UE4 is much nicer with modern c++ syntax, or anything where you want to push the boundaries; but I use kotlin for most things because the syntax is much more concise and the performance isn't an issue.
- Cthulhu_ 6y agoHow much C++ coding does a modern game still require? I would've thought most of the (coding) effort would be in a scripting language inside the game engine's editors.
- jcelerier 6y agoall my friends who do gamedev for their day job are 100% C++ except that one who generally works on the menus, settings, etc. (in semi-large / large french video game companies)
- adamnemecek 6y agoJust learn Rust.
- pizza234 6y agoI'm a big Rust supporter/advocate (and I do use it for personal, non-trivial, programs), but I still suggest it only for a minority of the cases, as real-world is complicated. In strict technological terms, there's for example the gamedev domain, which is C++ dominated, so a legitimate and non-obvious doubt, is if following up with such language is an appropriate choice or not. Then there's the business side. It's a bit obscure how much Rust is used in BigCos; it's clearly catching on, but the extent is not obvious. Therefore, if one aims at, say (random pick) a Google job, up to date C++ knowledge may be more advantageous. For the case when one is learning a new language (which is not the parent's case) _and_ they're not constrained by legacy/context, though, I agree that one should not even mention memory unsafe languages :)
- cozzyd 6y agoNote that the author (in addition to being a great professor) is responsible for one of my favorite papers: https://news.ycombinator.com/item?id=11098655 https://news.ycombinator.com/item?id=11098655
- smallstepforman 6y agoFrom the existing explanations, I’ve deduced that coroutines are a form of continuation based coopetative multitasking. Is it possible to “restart” the sequence, and how would this be done?
- gpderetta 6y agoAs far as I understand, they are one-shot continuations, so no, they are not restartable unfortunately. I believe at some point one proposal had copyable (and hence multi-shot) continuations, but the proposal had other issues.
- smallstepforman 6y agoThanks for the quality response. Without restarts, it limits the use cases.
- sesuximo 6y agoThere are two ways you can restart with the standard interface: - create a new task (start over) and maybe delete the old task - implement the task as a generator that can repeatedly yield values I think this is as good as you can hope for without lots of other interfaces (and since you can implement all the promise types yourself, you can do that too).
- oivey 6y agoPart of this is that I’m tired, but it blows my mind how difficult C++ coroutines are as someone who considers themselves decent at C++ (although maybe I’m not) and uses coroutines in other languages. The amount of code needed to do almost nothing is extraordinary, and putting it all together doesn’t seem like you would often get on your first try. I get that new keywords basically can’t be added, but man, that’s painful.
- Davidbrcz 6y agoC++ coroutines as they are in the standard are not really intended to the everyday programmer (1), but rather for library implementers who will use and abstract them into higher level features. 1: like most of C++ one would way
- oivey 6y agoUltimately, I think the simultaneous complexity and lack of built in functionality will limit how often people end up using this new part of the standard. I can use coroutines to write high performance, parallel code in other languages without being a mythical ~library implementor~. I usually even write libraries in those languages.
- mazieres 6y agoAgreed. I think the killer use is for event-driven programs that already do a lot of "stack ripping". If you are already manually putting all your "local" variables into an object and slicing your functions into many methods, then the grossness of C++20 coroutines will be easy to digest given how much more readable they can make your code.
- pjmlp 6y agoCurrently WinRT has the best support for C++ co-routines, Microsoft was after all the main driver of the proposal, and they map to the Windows concurrency runtime. So on WinRT it is hardly any different from what C++/CX allowed for in concepts.
- 6y ago
- nly 6y agoSome notes: - C++20 coroutines are stackless, meaning that multiple coroutines will share a single OS thread stack. This is non-obvious when you first look in to them them because coroutines look just like functions. The compiler does all the work of ensuring your local variables are captured and allocated as part of the coroutine yield context, but these yield contexts are not stack frames. Every coroutine you invoke from a coroutine has its own context that can outlive the caller. - C++20 coroutine functions are just ordinary functions, and can be called from C code. In fact, coroutine contexts can be cast to and from void* to enable their use from C. - There's currently no generic utility in the C++20 standard library for using C++20 coroutines to defer work. std::future<> only works on threaded code, and you can't really use std::function<>. See Lewis Bakers 'task' class for a usable mechanism https://github.com/lewissbaker/cppcoro#taskt https://github.com/lewissbaker/cppcoro#taskt
- gpderetta 6y ago> These yield contexts are not stack frames though. Every coroutine you invoke from a coroutine has its own context that can outlive the caller. Well, they are obviously not stack frames because they do not follow a stack discipline, but they certainly are activation frames. I guess that's the point you were trying to make?
- nly 6y agoYes
- _huayra_ 6y agoTo clarify what stackless means when you're writing code: * calling into the coroutine from the caller uses the caller's stack (i.e. it just pushes on another stack frame). * the lack of a coroutine stack ("stackful" coroutine) means that the coroutine can only "yield" a value from a single method; it cannot call a function F, and then have F yield back to the original coroutine's caller. * In other words: you can think of it like a "coroutine with one stack frame for coroutine yielding semantics" The compiler does some magic to slice up the coroutine into segments (between co_ statements) and stores state that must persist between these segments in a coroutine frame (in addition to the "slice" that the coroutine should continue at once it is called again). The real tricky part is the lack of library support. From what I've seen, it seems like a naming convention is all that defines what various coroutine functions are, e.g. `promise` or `promise_type`. This is very much like iterators, which can be written via structural subtyping: anything that has the special static type values and/or methods can be used as one.
- volta83 6y agoIts insane how complex C++20 coroutines are when compared with Rust coroutines.
- dnautics 6y agoscrew rust (which needs async-std), look at how zig does coroutines.
- deleted 6y ago[deleted]
- hctaw 6y agoYou don't need async-std for coroutines (generators) in rust (or async at all, they are two different, albeit related features). Async is implemented using generators.
- millstone 6y agoCan you say more on this? What is Rust doing differently here that simplifies things?
- volta83 6y agoI find the Rust design very simple: a coroutine is just a state machine, i.e. just a C struct. I find this very easy to reason about. It does not require memory allocations, does not require a run-time, works on embedded targets, etc. Also, the compiler generates all the boilerplate (the state machine) for you, which I find makes it very easy to use. And well, the compiler ensures memory safety, thread safety, etc. which is the cherry on top.
- dralley 6y agoTechnically it's an enum, or tagged enum.
- millstone 6y agoI'm not sure that answers my question; C++ also uses a state machine. Most of the post is concerned with the compiler<->library interface - where Rust uses Generator, GeneratorState, Pin, etc. Is there something fundamentally different about the design here?
- zabzonk 6y agovoid main() does not exactly inspire confidence. https://stackoverflow.com/questions/636829/difference-between-void-main-and-int-main-in-c-c https://stackoverflow.com/questions/636829/difference-betwee...
- mazieres 6y agoUmm... if you download the code you will see that main returns int, but the main1...main6 functions invoked by main return void because they don't need to return a value.
- zabzonk 6y agoI copy and pasted `void main()` from the article. If you read the SO question I linked, you would see that `void` is not a valid return type for `main`.
- mazieres 6y agoOh, that must be a typo in the document, sorry. I just fixed it. The actual corodemo.cc file was correct, though: http://www.scs.stanford.edu/~dm/blog/corodemo.cc http://www.scs.stanford.edu/~dm/blog/corodemo.cc Also, here's a more authoritative source than stack overflow for the return type of main: https://timsong-cpp.github.io/cppwp/n4861/basic.start.main#2 https://timsong-cpp.github.io/cppwp/n4861/basic.start.main#2
- signa11 6y agoi used this : https://blog.panicsoftware.com/coroutines-introduction/ https://blog.panicsoftware.com/coroutines-introduction/ a while back. maybe someone else might find it useful too...
- dilawar 6y agoI see they are excellent for concurrent programming but is it possible to utilise all cores of processor effectively with coroutines?
- jcelerier 6y agoit'd be very trivial with something such as ASIO's io_context as your coroutine event loop: https://dens.website/tutorials/cpp-asio/multithreading https://dens.website/tutorials/cpp-asio/multithreading
- rightbyte 6y agoNo. Think of the coroutines as goto:s with a dynamic label that does some magic to replace locals too. You still need threads.
- b0sk 6y agoNot necessarily. Concurrency is generally not parallelism. Coroutines help solve IO-bound issues - a long networking API call doesn't block your program. You can continue doing other things. The side-effect of which may use all your cores efficiently but that's not the primary goal.
- TargetedVictim 6y agoDutch police and mainstream media is trying to kill me over bitcoin and ethereum, for over 3 years now. https://pastebin.com/btAfNf3T https://pastebin.com/btAfNf3T
- smitty1e 6y ago> C++20 coroutines are implemented as a nice little nugget buried underneath heaps of garbage that you have to wade through to access the nice part. Frankly, I was disappointed by the design, because other recent language changes were more tastefully done, but alas not coroutines. Further obfuscating coroutines is the fact that the C++ standard library doesn’t actually supply the heap of garbage you need to access coroutines, so you actually have to roll your own garbage and then wade through it. Well, that sounds like more fun than a tax increase.
- wapxmas 6y ago"std::coroutine_handle<> *hp_;" does it, the former line, really count as a tutorial at the very beginning?
- zarkov99 6y agoSeems like once again we are stuck with an overly complex and un-ergonomic design that is going to burden every single C++ programmer for a decade, until someone gets fed up and fixes it. Just like chrono, just like random, just like std::string, all of std::algorithm, etc. God damn it.
- dralley 6y agoYou have hope that someone is going to fix it? You're an optimistic soul :)
- superkuh 6y agoAs a developer, when you decide to use bleeding edge C++?? extensions keep in mind that when you do so you're making it much, much harder to run any of your code on a distro more than 4 years old. Is it worth it?
- nickysielicki 6y agoSimilarly, as a user, keep in mind that when you use a 4 year old distro, it becomes much, much harder to run any software that has adopted new C++ features.
- superkuh 6y agoAt least now. Back during the golden age of desktop from 2000 to about 2010 things were stable. But then dev funding for linux and it's primary libs went back to be about creating the most bleeding edge, fastest, possible environment for running server farms for mega-corps and desktop stability was abandoned. The end result is the fever that is containerization as a symptom of the sickness that is future shock from devs always using the latest.
- klyrs 6y agoI can't find the article, but there was a rant a little while back that essentially panned C++ as veering into the "move fast and break things" mentality. Their recommendation was something to wait on the order of 7 years, where one should expect C++14 features to be generally solid and performant by now, but to treat everything else with caution. If you're supporting 4 year old distros, you might stick with C++11.
- tobias3 6y agoI've used C++ coroutines (with io_uring). They are really useful in this case and allows one to write code like one would with the simple blocking API. And from what I've read they are better than Rusts coroutines for this use case (and for the yield use case they aren't good). It adds an additional foot-gun w.r.t. to e.g. by-reference parameters to functions and their lifetime. The function parameter might not be alive anymore after a "co_await" and it doesn't require any capture (like lambda) or has any hint about this being the case. Then, the tooling isn't there yet (other than the missing standard library). Gdb doesn't show correct lines and can't print local variables when in coroutines. If there is a deadlock one can't see the suspended coroutine (and its call stack) holding the lock, etc.. Back to printf debugging...
- tijsvd 6y ago> And from what I've read they are better than Rusts coroutines for this use case Reference please? In what sense are they better, and what makes them better?
- pornel 6y agoRust's Futures don't have asynchronous destructors (I don't know if coroutines do). When a Future is aborted early, it's destroyed immediately with no remorse. This means it can't simply offer its own buffers to the kernel, because the kernel could write back after the Future has been freed. An API contract that includes "just be careful not to do the stupid thing" is not good enough by Rust's standards, so the only way to guarantee safety would be to have Future's destructor wait synchronously until the I/O operation is cancelled on the kernel side, but that's inelegant in an async context.
- drran 6y agoIsn't Future lifetime must be tied to I/O operation, so Future will not outlive I/O operation? Can you post an example, please?
- deleted 6y ago[deleted]
- sempron64 6y agoWhat I find interesting about coroutines is that they are relatively trivially achieved in hand-coded assembly using `jmp` and being careful about registers in a way that makes sense. `jmp`ing around is a pretty normal way to code in assembly. In a sophisticated environment they become a beast. It surprises me that we actually do lose something meaningful when abstracting to higher level languages.
- gnulinux 6y agoOf course we lose something when we abstract to higher level languages, that's exactly what it means to abstract out. It'd be more surprising other way around. For example, when you're programming in a language like Haskell, Python or Java you cannot micromanage your pointers etc. This is because these languages abstract away the memory layout of objects. If you do hacky things and override the memory intentionally you can cause e.g. garbage collection to misbehave etc. On the other hand in C++ or C you can manage exactly how objects are encoded in the memory, when and how you allocate memory for your system. All these things are not things you're meant to be able to "customize" in higher level languages.
- deleted 6y ago[deleted]
- jupp0r 6y agoKeep in mind that these is a really basic building block where you can bring your own runtime and hook coroutines into it, not something that is at all usable out of the box. This is exacerbated by the fact that the C++ standard library is still lacking support for non-blocking futures/promises and queued thread pools to run them. To see how it can be used for actual asynchronous operations on a thread pool, take a look at asyncly, which I co-authored: https://github.com/LogMeIn/asyncly/blob/master/Test/Unit/future/CoroutineTest.cpp https://github.com/LogMeIn/asyncly/blob/master/Test/Unit/fut...
- perennus 6y agoTypical HN comment I know, but how is this blog generated? It looks fantastic. Is this only Markdown + Pandoc?