Coroutines have been used in programming since 1958 and were defined by Knuth as a way to generalize a subroutine. While regular subroutines start at the beginning and end at the end, coroutines can pause their execution and resume later from where they left off. An explanation of this can be found here.

The topic is complex because we normally mix "what a coroutine is" with "how it is going to run".

How

There are two common ways to use coroutines:

  • stackless, where the code is transformed into a state machine by the compiler.
  • stackful, also known as fibers, which are user-mode threads that run on top of a regular operating system thread.

What

We can identify two types of coroutines based on their need for a stack:

  • stackless, only require a stack while the coroutine is running. Once the coroutine suspends, its local variables can be serialized into a fixed-size structure, and the current call stack can be used for executing the next coroutine.
  • stackful, allows you to suspend your coroutines at any point, providing more flexibility than stackless coroutines.

The lack of a stack is what allows the compiler to transform and use a stackless coroutine, while the need for a stack requires a runtime.

Conclusion

When you use async/await in your code, the compiler generates a state machine that will be executed later. This approach is possible because async/await only suspends execution at predetermined points, eliminating the need for a stack to track progress. The compiler can transform your code into a state machine that preserves the state of local variables and resumes execution from where it left off.

In contrast, when using goroutines, fibers, or green threads, you can suspend and resume execution at any point in the code. This flexibility necessitates a stack to keep track of the execution state. These types of coroutines also require a runtime environment capable of managing the stacks and scheduling the coroutines across available threads.