Coroutine
Coroutines — The Complete Guide
Learn what coroutines are, how they differ from threads, and how to use them for lightweight, non-blocking concurrency across languages.
Introduction
A coroutine is a special function that can pause its execution and resume later, without blocking the thread it runs on. Unlike an ordinary function that runs from start to finish in one go, a coroutine can suspend at a certain point, let other work happen, and then continue exactly where it left off — remembering all its local state.
Real-World Analogy
The Chef Who Multitasks
A single chef (one thread) puts a pot on to boil, and instead of standing idle, starts chopping vegetables. When the water is ready, they return to it. The chef never cloned themselves — they just paused one task and switched to another. That is exactly how a coroutine cooperates on a single thread.
Why Use Coroutines?
Benefits
- Extremely lightweight — thousands can run on one thread
- Non-blocking I/O without callback hell
- Sequential, readable code for asynchronous logic
- Lower memory and context-switch overhead than threads
- Fine-grained cooperative scheduling control
Threads Alone
- Each thread costs ~1 MB stack memory
- Expensive OS-level context switches
- Hard-to-debug race conditions and locks
- Limited scalability for many concurrent tasks
- Blocking calls waste CPU while waiting
Prerequisites
Before You Start
- Solid understanding of functions and return values
- Basic idea of concurrency vs. parallelism
- Familiarity with at least one language runtime (Python 3.7+, Kotlin, JavaScript, or C++20)
- Comfort with the difference between blocking and non-blocking I/O
- An IDE or terminal to run and test async code
Core Concepts
Suspension
The ability to pause at a defined point.
At a suspension point the coroutine saves its state and hands control back to the scheduler, freeing the thread to do other work.
Resumption
Continuing exactly where it stopped.
When the awaited work is ready, the coroutine resumes with all its local variables intact — as if it never paused.
Cooperative Scheduling
Coroutines yield voluntarily.
Unlike threads that the OS pre-empts, coroutines give up control only at suspension points, making their behavior predictable.
Coroutine vs Thread
| Aspect | Coroutine | Thread |
|---|---|---|
| Managed by | Language runtime / scheduler | Operating system |
| Memory cost | A few KB or less | ~1 MB stack each |
| Switching | Cooperative (at suspension points) | Pre-emptive (OS decides) |
| Count feasible | Hundreds of thousands | Usually a few thousand |
| Best for | I/O-bound tasks | CPU-bound parallel tasks |
Coroutines in Python (asyncio)
Python uses async def to declare a coroutine and await to suspend at an awaitable point.
import asyncio
async def fetch_data(name, delay):
print(f"Start {name}")
await asyncio.sleep(delay) # suspension point (non-blocking)
print(f"Done {name}")
return f"{name}-result"
async def main():
# Run two coroutines concurrently on one thread
results = await asyncio.gather(
fetch_data("A", 2),
fetch_data("B", 1),
)
print(results)
asyncio.run(main())
Coroutines in Kotlin
Kotlin has first-class coroutine support via the kotlinx.coroutines library and the suspend keyword.
import kotlinx.coroutines.*
suspend fun fetchData(name: String, delay: Long): String {
println("Start $name")
delay(delay) // suspending function
println("Done $name")
return "$name-result"
}
fun main() = runBlocking {
val a = async { fetchData("A", 2000) }
val b = async { fetchData("B", 1000) }
println(listOf(a.await(), b.await()))
}
Coroutine-style async in JavaScript
JavaScript uses async/await built on Promises — conceptually the same pause/resume model.
function fetchData(name, delay) {
return new Promise(resolve => {
console.log("Start " + name);
setTimeout(() => {
console.log("Done " + name);
resolve(name + "-result");
}, delay);
});
}
async function main() {
const results = await Promise.all([
fetchData("A", 2000),
fetchData("B", 1000),
]);
console.log(results);
}
main();
Coroutines in C++20
C++20 introduced coroutines with the keywords co_await, co_yield, and co_return.
#include <coroutine>
#include <iostream>
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
Task example() {
std::cout << "Before suspend\n";
co_await std::suspend_always{}; // pause here
std::cout << "After resume\n";
}
How a Coroutine Works Internally
- The coroutine starts executing like a normal function.
- At a suspension point (
await/yield), it saves its state (local variables, position) into a heap-allocated frame. - Control returns to the scheduler / caller.
- When the awaited result is ready, the scheduler resumes the coroutine from the saved frame.
- Execution continues until the next suspension point or completion.
Why Coroutines Scale Better
If each thread needs \(M_t\) memory and each coroutine needs \(M_c\) memory (with \(M_c \ll M_t\)), then for \(N\) concurrent tasks the total memory is:
\[ \text{Mem}_{\text{threads}} = N \times M_t \qquad\qquad \text{Mem}_{\text{coroutines}} = N \times M_c \]
Since \(M_c\) is often a few KB versus ~1 MB for a thread, coroutines can handle orders of magnitude more concurrent tasks in the same memory budget.
Types of Coroutines
| Type | Description |
|---|---|
| Stackful | Has its own stack; can suspend from nested calls (e.g., Lua, Boost.Coroutine). |
| Stackless | No separate stack; suspends only in the coroutine body (e.g., Python, C++20, Kotlin). |
| Symmetric | Transfers control directly to another coroutine. |
| Asymmetric | Always returns control to its caller/scheduler (most common). |
Best Practices
Do This
- Use coroutines for I/O-bound work; use threads/processes for CPU-bound work
- Never call blocking functions inside a coroutine — use their async equivalents
- Always handle cancellation and timeouts explicitly
- Structure concurrency (parent scope owns child coroutines) for clean cleanup
- Avoid shared mutable state; prefer message passing / channels
- Propagate and handle exceptions from awaited coroutines
Common Mistakes
time.sleep(2) inside an async coroutine — it freezes the whole event loop.
await asyncio.sleep(2), which yields control while waiting.
await a coroutine — it never runs and you get a "coroutine was never awaited" warning.
Interview Questions
| Question | Short Answer |
|---|---|
| What is a coroutine? | A function that can suspend and resume its execution, enabling cooperative concurrency. |
| How is a coroutine different from a thread? | Coroutines are runtime-managed and cooperative; threads are OS-managed and pre-emptive. |
| What is a suspension point? | A place (like await) where the coroutine pauses and yields control. |
| Stackful vs stackless coroutines? | Stackful have their own stack and can suspend from nested calls; stackless cannot. |
| When should you use coroutines? | For high-concurrency I/O-bound tasks where blocking threads would be wasteful. |
Quick Revision
| Language | Declare | Suspend |
|---|---|---|
| Python | async def |
await |
| Kotlin | suspend fun |
delay() / await() |
| JavaScript | async function |
await |
| C++20 | returns coroutine type | co_await / co_yield |
Key Takeaways
A coroutine is a pausable, resumable function that enables lightweight concurrency on a single thread. It shines for I/O-bound workloads, uses far less memory than threads, and keeps asynchronous code readable and sequential.