Table of Contents

    Coroutine

    CONCURRENCY & PROGRAMMING

    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.

    In one line: A coroutine is a function you can pause and resume, enabling concurrency without the heavy cost of threads.

    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

    1

    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.

    2

    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.

    3

    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())
    Note Even though task A "waits" 2 seconds, task B runs during that wait — total time is about 2 seconds, not 3.

    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

    1. The coroutine starts executing like a normal function.
    2. At a suspension point (await / yield), it saves its state (local variables, position) into a heap-allocated frame.
    3. Control returns to the scheduler / caller.
    4. When the awaited result is ready, the scheduler resumes the coroutine from the saved frame.
    5. 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

    Bad Calling a blocking function like time.sleep(2) inside an async coroutine — it freezes the whole event loop.
    Good Using the non-blocking equivalent: await asyncio.sleep(2), which yields control while waiting.
    Bad Forgetting to 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.