7 ms·
Why implement coroutines when you have multithreading? Coroutines are to me a brain-twisty way to make the most out of a single core, because your language doe
by danuker 4y ago
Why implement coroutines when you have multithreading?
Coroutines are to me a brain-twisty way to make the most out of a single core, because your language doesn't support multithreading. For example: JavaScript with a single thread in the browser, Python with the GIL.
C++ has threads which ACTUALLY run in parallel on the CPU. Why bother complicating the language further?
- deleted 4y ago[deleted]
- deleted 4y ago[deleted]
- steveklabnik 4y agoWhy use a hammer when you already have screwdrivers?
- com2kid 4y ago> Why implement coroutines when you have multithreading? Because coroutines are easier to think about than threads, and avoid entire (large) classes of bugs. Because coroutines (typically) have a lower overhead than threads. Because if you are IO bound and not CPU bound, and therefore you don't need to spread work across cores, threads may not provide a given workload with much, or any, benefit.
- sgtnoodle 4y agoI've benefited from cooperative userspace context switching with CPU bound workloads as well. Imaging simulating a vehicle's worth of embedded software, with several dozen tasks originally intended to run at 100Hz asynchronously across multiple different computers. Now, imagine running them faster than real time, deterministically. Now imagine doing several thousand of those simulations. The overhead of OS threads, context switching and synchronization starts to add up, and is 10-100x slower for that sort of workload.
- com2kid 4y agoCooperative multitasking systems having more deterministic performance profiles is often overlooked. I'm working on a post right now covering this in detail, but I spent the last 2 hours being reminded that explaining concurrency stuff in simple terms is hard. :-D
- akmittal 4y agoReal threads can too expensive sometimes. A single coroutine in Go just use few kb of memory. So we can easily spawn hundred of thousands of coroutines but not possible using threads
- dragontamer 4y agoYour point is correct but your magnitudes are off. IIRC, a thread takes up ~32kB or so worth of data in pthreads/Linux, or at least... somewhere around there. So on a rather cheap server with 64GB of RAM, you can easily fit 1-million threads. More expensive servers have 512GB, 1TB or more RAM and can easily handle this kind of load. Of course, coroutines are even smaller/more efficient and can go into 10-million, 100-million or higher.
- Matheus28 4y agoA thread on Windows is MUCH heavier than Linux. 1 MB stack from what I remember plus takes a lot longer to create.
- dragontamer 4y agodwStackSize can be set as low as 64kB though. So if you're in the habit of making millions-of-threads, maybe make them smaller on Windows? It does default to 1MB, but these sorts of things are configurable.
- klabb3 4y agoA bigger issue is creating and destroying threads, which can be costly if you don’t do a lot of work per each. Of course, people have worked around this problem for ages with thread pools, but then you’re already breaking up ownership, code flow and screwing up the stack for debugging and panicking. So something like coroutines or green threads is still motivated, it is just extremely different across languages and is almost always a leaky abstraction.
- com2kid 4y agoUgh I really need to finish my blog post where we had async with only a few dozen bytes of overhead per task. Cooperative async runtimes are obscenely efficient.
- eestrada 4y agoBecause native multithreading is expensive when it comes to (a) memory consumption per thread of execution and (b) context switching. Also, if your program spends a lot of time just waiting on outside resources (most notably slow/blocking IO), coroutine concurrency is bother faster and more lightweight. If your program is CPU bound, then coroutines/async have little-to-no value over native multithreading. That being said, yeah, most async workflows are obtuse and difficult to follow and they force all consumers of your interface to also use the async primitives if they want the benefits. Go seems to be one of the few languages that does async/coroutines right. See: https://eli.thegreenplace.net/2018/go-hits-the-concurrency-nail-right-on-the-head/ https://eli.thegreenplace.net/2018/go-hits-the-concurrency-n...
- rcme 4y agoWhat I like about Go is that each co-routine is entirely synchronous, and the only concurrency primitives you're given are channels and mutexes. This makes your code so simple and easy to understand. I just looked up Kotlin's co-routines, and there are a number of annoying hoops you need to jump through: 1. Co-routines can only be built by calling runBlocking or coroutineScope. 2. Functions that are async buy should act like blocking calls in a co-routine need to be marked with `suspend` 3. There are a bunch of primitives you need to use like `launch` or `async`. 4. There are built-in options to make the co-routines lazy, but it makes the API feel sloppy. 5. Rather than locks, Kotlin encourages explicit thread dispatching. Some times you need to do this in Go too, like when using a C library, but in general it's rare.
- ackfoobar 4y ago`launch` is just the `go` keyword. `async` stores the result in a `Deferred` (or `Promise`), which is an abstraction for parallel decomposition. Other than confining UI work to the main thread, I don't think I have seen thread confinement as an alternative to locks.
- rcme 4y agoThat's kind of my point. Go has the `go` keyword. Kotlin has runBlocking, coroutineScope, launch, async, and suspend. Go's simplicity feels nicer to me.
- deleted 4y ago[deleted]
- okamiueru 4y agoTo more elegantly write code that is async in nature like IO/Web requests? Let stuff happen and just await where needed? Seems like different type of concern than what you need multiprocessing for.
- injidup 4y agoCoroutines don't really have anything to do with threads. They are just a way for you to model suspending and resuming functions. If you have every tried to implement a complicated iterator you will find that you model a for loop in your head but that the iteration of the loop is controlled from outside using the ++ operator. The ++ operator allows the iterator loop to go one more time round. Normally you have to manage this with state stored in some object. With coroutines you can actually write a loop and co_yield from the middle of the loop to implement the ++ operator.
- grogers 4y agoThis is actually really convenient. In our code we have an abstraction that wraps a set using a variant. Instead of implementing an iterator/begin/end based on which data the variant holds, we have an iter() method that unwraps the variant and loops through and co_yields. Since you can consume a generator in a for-each loop, you use it the exact same way (except calling iter). In our case it doesn't save that much complexity, but it's definitely some. I could imagine more complicated cases where it is even more convenient.
- oldgradstudent 4y agoEveryone who ever programmed a bit in Python knows that. Generators simplify your code and remove unnecessary boilerplate. However, async/await is real nice as well.
- mrtracy 4y agoCo-routines can be a great structured-programming tool for implementing state machines. A good article which explains this idea explicitly: https://eli.thegreenplace.net/2009/08/29/co-routines-as-an-alternative-to-state-machines https://eli.thegreenplace.net/2009/08/29/co-routines-as-an-a... That said, overhead always needs to be considered; the overhead cost of using a CPP co-routine would be considerable for implementing a state machine that transitions rapidly.
- deleted 4y ago[deleted]
- bmurphy1976 4y agoGood for embedded devices (e.g. a single core ESP32).
- demindiro 4y agoFor an I/O heavy application (not in C++) I avoid threads for the main "loop" as there is a central cache that is accessed often. Synchronizing access to it is likely more expensive and error-prone than using just a single thread. It is also possible a few hundred or thousand tasks are running concurrently, which becomes very expensive very quickly with threads. There are a few CPU-intensive tasks but those are easy to isolate and send to a threadpool.
- GuB-42 4y agoCoroutines are when you don't want to run in parallel, because sequence matters. A typical example is iterators, you want the loop and iterator (routine and coroutine) to run in lockstep. You could use threads instead, but then you risk getting in all sorts of concurrency problems. And if you synchronize everything, then great, you have reinvented coroutines. In fact, you can think of coroutines as threads with built-in synchronization, which may be how they are implemented.
- 112233 4y agoCoroutines This is another can be used It as another way is still possible to language construct like manage object lifetime, and make some to use threads control flow much easier if you have coroutines to write. lambdas.
- jcelerier 4y agoeven without any threads or async, coroutines are very nice for writing generators and super useful when you want generic code which can iterate, without leaking the container implementation to client code
- dragontamer 4y ago> C++ has threads which ACTUALLY run in parallel on the CPU. Why bother complicating the language further? Actual parallelism is surprisingly slow and resource heavy in practice. When everything is on one core, you keep things local in L3, L2, L1. However, add on a 2nd core, then you suddenly have "ping ponging". Lets say core#0 has data in L1 cache, and now Core#1 needs to read-modify-write to it. That means the cores need to: 1: Core#1 begins to mark that cache-line as exclusive. 2. Core#0 needs to then respond by marking the line as invalid. Then Core#0 ejects the data from L1, and passes it to Core#1. 3. Core#1 can finally begin to work on the data. 4. If Core#0 uses the data again, Core#1 must do step#2 again. -------- This is called "Ping-ponging". L1 cache read/writes is 1-nanosecond, but ping-pongs can be 30nanoseconds or slower, 30x slower for no reason. You don't want to add another core to your problem unless you're _actually_ getting a speed benefit. Its more complex than it may seem at first glance. You very well could add a bunch of cores and then suddenly your program is way slower because of these kinds of issues. Another problem: False sharing. Core#0 is working on "int x", and Core#1 is working on "int y", but x and y are on the same cacheline. So they have to ping-pong even though they never actually touch the same data.
- PaulDavisThe1st 4y agoYour example implies some combination of (a) excessive data sharing between threads (b) possible need for core pinning. If thread A is going to need to read all the data touched by thread B, then it's unclear why you've split the task across threads. If there are still good reasons, then probably pin A and B to core N (never pin anything to core 0, unrelated story), and let them interleave there. If that doesn't make sense, then yep, you'll have to face the cost of ping-ponging.
- jb1991 4y agoThis is not so likely in most workloads unless multiple threads are hitting very close or adjacent regions in memory. Most multithreading workloads work on memory not so close together. It's good to be aware of it, but to suggest it is the most likely outcome from multithreading is misleading.
- samsquire 4y ago
- layer8 4y agoCoroutines are for when you would normally use callbacks or a state machine, but handling the callback’s state or the state machine is complex enough that it becomes much simpler when expressed as a coroutine (simpler to reason about). It isn’t really an alternative to multithreading.
- mrtracy 4y agoCo-routines (when used as an async primitive) are an an alternative to callbacks. Callbacks exist to avoid the overhead of marshalling async results back to the original calling thread: instead of having the calling thread poll until the child thread(s) pass their results back, the calling function returns after kicking off the children, and one of the children directly resumes the work directly when the results are present. This is a significant performance gain. However, callbacks have famously poor ergonomics, so many structured alternatives have been created: Promises/Futures, Async/Await, and CPP Coroutines.
- opportune 4y agoCoroutines are about decoupling OS-level threads from logical threads (where OS-level threads are already decoupled from physical threads). The reasoning is actually very similar to why OS-level threads do not correspond directly to physical threads. You may wonder why OS threads are not themselves sufficient, but it turns out there is a decent amount of overhead involved in managing huge numbers of OS threads. There are limits at which Linux does not perform well anymore, leading to problems like live locking. Also, if you can avoid context switching/cache invalidation by keeping a single OS-level thread busy, you get performance benefits even before you hit OS limits. So the userspace scheduling and green threading that coroutines enable allow you to increase concurrency without hitting these limits.
- MathMonkeyMan 4y agoIt'd be nice if we had system-level "lightweight threads." The explanation I've heard before is that "user space knows more about the program." So people write clever runtimes that do kernel-like task scheduling multiplexed over CPUs, in user space. I can buy that. Think about a system thread. It's pretty much its own process that shares an address space with others. Its interface is syscalls and memory. A user thread (coroutine, goroutine, greenlet, whatever) is... whatever you want it to be. It can be small and special-purpose.
- klabb3 4y agoI agree, but I think as you’re alluding to, the deeper issue is that the syscall boundary is too big of a fence to jump over for small operations. Hence, io_uring and similar, which pools multiple logical syscalls into one. The alternative way would be to have non-syscalls but kernel-aware libraries that share memory with the kernel, I guess? It’s a much bigger change, philosophically, so I think the io_uring model + user space scheduling will take the throne for the next decade or so at least. It’s a bit unfortunate because we’ll likely see a lot of bloated and fragmented language runtimes, but it’s still vastly superior to what we’ve seen over the last decade+.