Coroutines examined use cases implementation critique
posted on 09 Oct 2025 under category programming
Date | Language | Author | Description |
---|---|---|---|
09.10.2025 | English | Claus Prüfer (Chief Prüfer) | Coroutines Examined: Use Cases, Implementation Patterns, and Practical Critique |
The rise of asynchronous programming patterns has transformed how we think about concurrency in modern software development. Coroutines, in particular, have gained significant attention across multiple programming paradigms—from Python’s generators to C++’s stackless coroutines. However, their applicability varies dramatically depending on the domain. This article examines coroutines from multiple perspectives and evaluates their suitability for different use cases.
Coroutines are program components that generalize subroutines for non-preemptive multitasking by allowing execution to be suspended and resumed. Unlike traditional functions that run to completion, coroutines can yield control (return data to the caller) while maintaining their execution state.
The implementation and behavior of coroutines differ significantly between interpreter-based and compiled languages.
Python’s coroutine implementation is built on top of its generator mechanism, which itself relies on the Python interpreter’s execution model:
Characteristics:
yield
, yield from
, async
/await
)Example - Recursive Generator Pattern:
The following example demonstrates a recursive generator pattern used in the python-xml-microparser for hierarchical XML element traversal:
class Element:
"""XML Element with recursive iteration capability."""
def __init__(self, name, content=''):
self.name = name
self.content = content
self._child_elements = []
def __iter__(self):
"""Overloaded iterator for child elements."""
return iter(self._child_elements)
def add_child_element(self, element):
"""Append element to children."""
self._child_elements.append(element)
def iterate(self):
"""Recursive generator through hierarchical objects."""
yield self
for child in self:
for descendant in child.iterate():
yield descendant
# usage example
root = Element('config')
vhost1 = Element('vhost', 'server1.example.com')
vhost2 = Element('vhost', 'server2.example.com')
location = Element('location', '/api')
root.add_child_element(vhost1)
root.add_child_element(vhost2)
vhost1.add_child_element(location)
# recursively iterate through all elements
for element in root.iterate():
print(f"Element: {element.name}, Content: {element.content}")
# output:
# element: config, content:
# element: vhost, content: server1.example.com
# element: location, content: /api
# element: vhost, content: server2.example.com
This pattern demonstrates true generator-based coroutines where yield
provides suspension points for hierarchical traversal without async/await complexity.
Pros:
yield
statementsCons:
yield
semanticsC++ coroutines (standardized in C++20) represent a fundamentally different approach:
Characteristics:
Example:
#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 process_stream() {
co_await std::suspend_always{};
// processing logic here
}
Pros:
Cons:
Coroutines shine in scenarios involving continuous data streams and I/O-bound operations where waiting dominates processing time.
Scenario: Processing video streams, network packet analysis, or real-time sensor data
Why coroutines help:
Example use case: Video transcoding where frames are processed as they arrive, allowing early frames to begin encoding while later frames are still being received.
Scenario: Database queries, file operations, network requests
Why coroutines help:
Example: A web scraper processing thousands of URLs—while one request waits for network response, others can proceed, maximizing throughput.
Scenario: Real-time audio synthesis, effects processing, streaming audio
Why coroutines help:
Example: Digital audio workstation processing multiple audio tracks in parallel, where each track’s processing can yield while waiting for next buffer.
Scenario: Scientific computing, big data analytics, machine learning on large datasets
Why coroutines help:
Example: Processing terabytes of log files for anomaly detection, yielding alerts as they’re discovered rather than waiting for complete analysis.
Not all concurrency problems benefit from coroutines. Some scenarios involve overhead without corresponding benefits.
Scenario: Request-response protocols, RPC systems, control messages
Why coroutines are inappropriate:
Analysis: A message arrives, gets processed, generates response—complete transaction. The suspension/resumption overhead of coroutines adds complexity without benefit. Simple state machines or event handlers are clearer and more efficient.
Example: HTTP request handling—the entire request is typically available before processing begins. There’s no advantage to suspending mid-processing.
Scenario: Configuration loading, small file operations, database record processing
Why coroutines are inappropriate:
Analysis: When data is small and processing is fast, the complexity of coroutine state management outweighs any benefits. Traditional synchronous code is clearer, more debuggable, and often faster.
Example: Parsing a 1KB JSON configuration file—loading and parsing complete in microseconds. Introducing coroutines adds complexity without improving performance or clarity.
def fibonacci_generator():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# usage
fib = fibonacci_generator()
print(next(fib)) # 0
print(next(fib)) # 1
print(next(fib)) # 1
print(next(fib)) # 2
C++ doesn’t have a direct generator equivalent in the traditional sense, but C++20 coroutines can implement generator patterns:
How they fit into coroutines:
Pros:
Cons:
Example:
#include <coroutine>
#include <optional>
template<typename T>
class Generator {
public:
struct promise_type {
T current_value;
Generator get_return_object() {
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle;
bool next() {
handle.resume();
return !handle.done();
}
T value() {
return handle.promise().current_value;
}
~Generator() { if (handle) handle.destroy(); }
};
Generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
int temp = a;
a = b;
b = temp + b;
}
}
Boost has been a pioneer in bringing advanced C++ features to developers before standardization.
Boost.Coroutine (now deprecated in favor of Boost.Coroutine2) provided stackful coroutines:
Key characteristics:
Example:
#include <boost/coroutine2/all.hpp>
#include <iostream>
void cooperative(boost::coroutines2::coroutine<int>::push_type& sink) {
for (int i = 0; i < 10; ++i) {
sink(i); // yield value to caller
}
}
int main() {
boost::coroutines2::coroutine<int>::pull_type source{cooperative};
for (auto i : source) {
std::cout << i << " ";
}
}
In my opinion, the Boost pattern provides:
The Boost implementation prioritizes usability and reliability over theoretical purity. For most applications requiring coroutines in C++, Boost.Coroutine2 provides everything needed.
C++20 introduced native coroutine support with a different philosophy:
Key differences from Boost:
Example:
#include <coroutine>
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() {
co_await std::suspend_always{};
// coroutine body
}
Advantages over Boost:
Disadvantages:
Beyond Boost and C++20, several libraries explore coroutine patterns:
These libraries demonstrate various trade-offs between performance, usability, and feature sets.
Coroutines introduce unique security considerations that developers must address:
The widespread adoption of async/await patterns in modern web frameworks represents a fundamental misapplication of coroutine concepts. The industry has embraced complex syntax without recognizing that simpler alternatives exist and work better.
Web applications follow a simple pattern that requires no coroutine concepts:
Example of unnecessary async/await complexity:
The following code demonstrates an asynchronous callback function loadData()
with multiple issues:
await
keyword is unnecessary because the operation is fundamentally synchronousloadData()
must be defined and called separately, adding complexityfunction UserProfile({userId}) {
const [data, setData] = useState(null);
useEffect(() => {
async function loadData() {
const userData = await fetchUserData(userId);
setData(userData);
}
loadData();
}, [userId]);
return data ? <Profile data={data} /> : <Loading />;
}
For typical web application scenarios, object-oriented programming patterns with straightforward callbacks provide clearer, more maintainable solutions than async/await.
Example from x0 Framework (sysXMLRPCRequest.js):
The x0 framework demonstrates a clean OOP approach where a callback function is executed in an instantiated object when data has been received:
// instantiate the xmlrpc request object
var RequestObject = new sysCallXMLRPC('/api/data');
// define the request object with callback
var ResultObject = {
PostRequestData: { userId: 123 },
XMLRPCResultData: null,
// simple callback function executed when data is received
callbackXMLRPCAsync: function() {
console.log('Data received:', this.XMLRPCResultData);
// process the received data
processUserData(this.XMLRPCResultData);
}
};
// execute the request
RequestObject.Request(ResultObject);
Why this approach is superior:
a) Much more readable: The callback pattern is explicit and easy to follow—you define the callback function directly in the request object, and it’s called when data arrives
b) Much simpler: No async/await syntax complexity, no promise chains, no hidden control flow—just a straightforward callback that executes when the XMLHttpRequest completes
c) True OOP: The callback is a method of the request object, maintaining encapsulation and object-oriented principles
This pattern from the x0 framework demonstrates that traditional OOP with callbacks is sufficient for web applications without introducing coroutine complexity.
Generators represent the only valid application of coroutines in web frameworks, specifically for processing large datasets where memory efficiency and lazy evaluation provide tangible benefits.
function* dataGenerator() {
let index = 0;
while (true) {
yield fetchNextChunk(index++);
}
}
// usage
const stream = dataGenerator();
for (const chunk of stream) {
processChunk(chunk);
if (shouldStop()) break;
}
This is the only legitimate use of coroutines in web frameworks—generators for processing large data sets where data has to be rendered before all data has been processed.