Article by Ayman Alheraki on January 11 2026 10:32 AM
With the introduction of coroutines in C++20, the language has significantly enhanced its capability to handle asynchronous programming, making it more competitive with modern languages like Go and its goroutines. Coroutines bring a more efficient, scalable, and elegant way to handle asynchronous tasks, offering a substantial improvement over traditional thread-based programming.
This article will dive deep into what coroutines are, how they work, how they impact multithreading in C++, and compare them with GoLang's goroutines.
Coroutines are generalizations of functions. While a normal function runs from start to finish and returns a result, a coroutine can suspend its execution at some point, returning control to the caller without losing its state. It can then resume its execution later, continuing from where it left off.
In C++, coroutines introduce three new keywords:
co_await: Pauses the coroutine until the awaited operation is complete.
co_yield: Yields a value and temporarily suspends the coroutine’s execution.
co_return: Completes the coroutine and returns the final result.
These coroutines are especially useful for:
Asynchronous I/O: Non-blocking network operations, file I/O.
Lazy evaluation: On-demand computation that yields values as needed.
Task parallelism: Better handling of multithreading and concurrent tasks.
C++ coroutines differ from traditional functions in that they allow the function to pause and resume its execution. This pausing and resuming happens in a way that is highly efficient in terms of memory and CPU usage.
When a coroutine is paused, its state (such as local variables and the current execution point) is preserved, meaning you don't need to worry about manually managing states across multiple function calls. Here's a simple example of how a coroutine works in C++:
struct CoroutineExample { struct promise_type { CoroutineExample get_return_object() { return {}; } std::suspend_never initial_suspend() { return {}; } std::suspend_never final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} };};
CoroutineExample my_coroutine() { std::cout << "Step 1\n"; co_await std::suspend_always{}; // Suspend here std::cout << "Step 2\n"; co_return;}
int main() { auto coro = my_coroutine(); std::cout << "Resume coroutine\n"; coro.resume(); // Resume the coroutine}Output:
vbnetCopy codeStep 1Resume coroutineStep 2In this example:
The coroutine suspends after printing "Step 1".
It resumes and prints "Step 2" when the .resume() method is called.
Coroutines are a powerful tool for concurrent and parallel programming, as they provide an efficient alternative to thread-based programming. Coroutines allow you to write non-blocking code in a sequential style, which is easier to read and maintain compared to thread-based code.
With coroutines, you don’t have to worry about thread synchronization, mutexes, or condition variables, since coroutines are designed to work in cooperative multitasking, where each coroutine voluntarily yields control when it’s not able to make progress (e.g., waiting for I/O).
std::future<void> async_task() { std::cout << "Async task starts...\n"; co_await std::suspend_always{}; std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout << "Async task resumed...\n";}
int main() { auto task = async_task(); std::cout << "Main function continues...\n"; task.wait(); // Wait for coroutine to finish}Output:
xxxxxxxxxxvbnetCopy codeAsync task starts...Main function continues...Async task resumed...
In this example, the async_task function runs concurrently with the main function, demonstrating how coroutines can handle non-blocking execution, which is essential for asynchronous programming.
Use Coroutines for Asynchronous Operations: They shine when it comes to handling I/O-bound tasks such as network requests, file operations, or GUI event handling.
Avoid Overusing Coroutines: Coroutines come with overhead, so use them when you genuinely need asynchronous behavior or need to split work across different phases.
Leverage Libraries: Libraries like libunifex and Boost.Asio can help you manage coroutine-based workflows more effectively.
Avoid Blocking Calls: Since coroutines work best with asynchronous tasks, avoid blocking the main thread inside a coroutine (e.g., using std::this_thread::sleep_for in coroutines should be replaced with non-blocking co_await).
Goroutines in Go serve a similar purpose to C++ coroutines, enabling lightweight concurrency. However, there are notable differences:
| Feature | C++ Coroutines | Go Goroutines |
|---|---|---|
| Concurrency Model | Cooperative multitasking, requires explicit co_await or co_yield to suspend. | Preemptive multitasking, handled by the Go runtime. |
| Ease of Use | More control but requires boilerplate code (e.g., promise types). | Simpler to use, with built-in support for channels and easier syntax. |
| Performance | Highly efficient with minimal overhead. | Optimized for concurrency but comes with higher overhead due to preemption. |
| Scalability | Can handle large numbers of concurrent tasks but needs careful management of resources. | Extremely scalable with Go’s lightweight threading model. |
| Synchronization | Requires explicit mechanisms (e.g., mutex, condition variable, or atomic). | Built-in synchronization through channels and select statements. |
One of the key advantages of coroutines over threads is that they are much more lightweight. Threads require stack memory, context switching, and OS-level scheduling, which comes with overhead. Coroutines, on the other hand, are designed to be non-blocking, allowing many more coroutines to run in parallel with much lower resource consumption than threads.
For example, running millions of threads is impractical due to resource limitations, while coroutines can handle that scale efficiently.
The introduction of coroutines in C++20 gives developers a powerful tool for writing asynchronous and concurrent code. It enhances multithreading capabilities by allowing more efficient and scalable handling of tasks, making C++ competitive with modern languages like Go.
While Go's goroutines are simpler to use, C++ coroutines offer fine-grained control and efficiency, which are beneficial for developers needing performance and precision in their applications. When used correctly, coroutines in C++ can dramatically improve the performance of asynchronous applications, especially when combined with the vast ecosystem of existing C++ libraries.
Example Usage:
Asynchronous network servers.
Background data processing.
Game development and real-time simulations.
Both coroutines and goroutines offer powerful solutions for modern software development, but C++'s coroutines give you the flexibility and performance you'd expect in high-demand applications.