i am totally ignorant on coroutines
can someone explain to me why i would want to use coroutines instead of just spawning and managing worker threads myself the regular way and handing them whatever task i want done concurrently in the form of some 'task' or 'executable' object?
It's been a while since I've thought about this so my reasoning is going to be less than airtight, but let me try. Keep in mind that I speak from the perspective of someone who's interested in designing concurrent systems. Coroutines have other uses besides enabling concurrency -- see for example "generators", as in Python's.
Coroutines and OS threads have one crucial thing in common: both store the execution context of a logical task.
For an OS thread, it's easy: the stack is where the execution context is stored. From the IP to the state of local variables, everything is stored in the stack.
For a coroutine, well, the particulars get really different depending on your programming language/framework of choice, but ultimately, it's still the same: just a chunk of memory where the code that implements the coroutine stores its state (its "locals", the "instruction pointer" -- the point at which the coroutine yielded last, -- etc.)
As an aside: this video is really really interesting if you want to understand what a "coroutine" really looks like, in terms of the data structure that represents its state, and in terms of how the coroutine's source code is lowered to a regular function (which is typically a state machine). Note that Rust's async functions are coroutines.
https://www.youtube.com/watch?v=ZHP9sUqB3Qs
The main difference then between coroutines and OS threads is in how and by who that "logical task" is scheduled to actually execute, and most importantly: cost.
When you create an OS thread, you rely on the OS scheduler to dispatch the subroutine designated as its entry point. Simple as, it's completely outside of your control.
Coroutines however require you to build a scheduler within your program. And unlike threads, they are cooperatively scheduled -- a coroutine only yields control at predefined points in its execution body, which means that if you screw up and write a coroutine that ever blocks on
anything (I/O, a mutex, etc) your whole program grinds to a halt.
Why would you ever prefer coroutines, when OS threads do everything they can, and do not require you to reimplement a scheduler within your program? The answer is cost.
>and handing them whatever task i want done concurrently in the form of some 'task' or 'executable' object?
Well here's the thing, when your "tasks" are utterly trivial but
massive in volume (a perfect case study are instant messaging servers, which are saturated by volume of tasks, but each task is no more than storing and relaying a tiny message from A to B), the real-world overhead imposed by OS threads becomes the limiting factor.
In the IM example, you can distill your task down to "Sender, Recipient, Message body" -- let's say all together 1KiB in size.
A typical OS will get 1
MiB of stack allocated to it, nevermind the kernel-internal data structures to keep track of it and schedule it. And the scheduling is going to be much more heavy-weight, too.
And you're going to have to call into the kernel who knows how many times (at the minimum you'll need to mmap a stack, clone() the thread, then glibc will probably do a bunch of syscalls in its prologue, then the thread needs to exit(), ....)
So there you have it. If your tasks are heavy-weight, then the benefits of using coroutines decrease -- the added complexity is not worth the returns. If your tasks are light-weight however, modelling them using OS threads wastes resources, unacceptably so depending on your requirements.