DEV Community

Cover image for C++20 Coroutines vs Event-Driven Frameworks: Two Approaches to Async Programming
Artak Avetyan
Artak Avetyan

Posted on

C++20 Coroutines vs Event-Driven Frameworks: Two Approaches to Async Programming

TL;DR: Technical deep-dive comparing C++20 coroutines and Areg SDK. Understand the trade-offs, use cases, and how they complement each other in modern C++ systems.

C++20 coroutines and Areg SDK represent two distinct approaches to async programming. Coroutines provide sequential syntax for async operations within a process. Areg provides fire-and-forget messaging with built-in IPC, network communication, and thread safety guarantees. They solve different problems and work well together.


The Async Programming Challenge

Every C++ developer has written blocking code like this:

auto data = fetchFromNetwork();  // Thread blocks
processData(data);               // Waits
saveToDatabase(data);            // Still waiting
Enter fullscreen mode Exit fullscreen mode

The thread sits idle. Resources waste. Throughput suffers.

Modern C++ offers multiple solutions. This article examines two: C++20 coroutines for sequential async syntax, and event-driven frameworks like Areg SDK for distributed messaging with built-in concurrency management.


C++20 Coroutines: Sequential Syntax for Async Operations

Core Mechanism

A coroutine is a function that can suspend execution and later resume. According to cppreference.com: "Coroutines are stackless: they suspend execution by returning to the caller."

Task<void> downloadAndProcess() {
    auto data = co_await fetchAsync();  // Suspends, returns to caller
    processData(data);                  // Resumes here when data arrives
    co_await saveAsync(data);           // Suspends again
}
Enter fullscreen mode Exit fullscreen mode

Execution flow:

  1. co_await saves coroutine state (local variables, execution point)
  2. Returns control to caller
  3. Caller thread continues other work
  4. When operation completes, someone calls handle.resume()
  5. Coroutine continues from suspension point

The Sequential Syntax Advantage

Coroutines eliminate callback pyramids and state machines:

Task<Result> processOrder(OrderId id) {
    auto order = co_await fetchOrder(id);
    auto inventory = co_await checkInventory(order.items);
    if (inventory.available) {
        co_await reserveItems(order.items);
        co_await chargePayment(order.payment);
        co_await shipOrder(order);
    }
    co_return order.status;
}
Enter fullscreen mode Exit fullscreen mode

Local variables persist across suspension points. Control flow reads top-to-bottom.

Parallel Execution Pattern

For concurrent operations, start multiple tasks before awaiting:

Task<void> parallelProcessing() {
    auto taskA = operationA();  // Starts immediately
    auto taskB = operationB();  // Starts immediately
    auto taskC = operationC();  // Starts immediately

    // All three running concurrently
    auto resultA = co_await taskA;  // Wait for results
    auto resultB = co_await taskB;
    auto resultC = co_await taskC;
}
Enter fullscreen mode Exit fullscreen mode

This is structured concurrency -- explicit parallelism with clear join points.

Infrastructure Requirements

C++20 provides the mechanism, not the infrastructure. You must implement:

  • Custom Task<T> type with promise_type and awaiter protocol
  • Executor/scheduler to manage coroutine resumption
  • Thread affinity system (coroutines have none by default)
  • Cancellation and timeout mechanisms
  • Custom allocators for embedded/performance-critical code

For distributed systems, add:

  • Serialization layer
  • IPC transport
  • Network protocols
  • Service discovery
  • Fault tolerance and reconnection logic

Production-ready coroutine systems require significant infrastructure investment before business logic development begins.


Areg SDK: Event-Driven Messaging with Built-In Distribution

Core Mechanism

Areg SDK implements Object RPC (ORPC) where service requests are fire-and-forget operations returning immediately. Responses arrive asynchronously as callbacks, automatically routed across thread, process, or network boundaries.

// Consumer - fire and forget
void MyComponent::initiateRequest() {
    mProxy.requestHelloWorld("Alice");  // Returns instantly
    // Continue other work immediately
}

// Response callback - delivered asynchronously
void MyComponent::responseHelloWorld(const String& greeting) {
    // Guaranteed to execute on component's owner thread
    LOG_INFO("Received: %s", greeting.getString());
}
Enter fullscreen mode Exit fullscreen mode

Three Levels of Asynchrony

Level Mechanism Thread Blocked? Caller Waits? Example
Blocking Synchronous call Yes Yes recv(socket)
Async/Await Coroutine suspension No Yes (logically) co_await read()
Fire-and-Forget Event dispatch No No proxy.request()

Areg operates at the highest asynchrony level -- requests never block or suspend the caller.

Built-In Infrastructure

Thread Affinity: Component callbacks always execute on the component's designated owner thread. No mutexes, no locks, no data races. Write single-threaded logic that runs safely in multi-threaded systems.

Automatic Service Discovery: When a provider starts, it broadcasts availability. Consumers are notified automatically. When a service crashes, consumers receive disconnect notifications and enter waiting state. Reconnection is automatic when the service returns.

Transport Transparency: Critical architectural feature. Same code runs identically for:

  • Inter-thread communication (same process)
  • Inter-process communication (IPC via shared memory/sockets)
  • Network communication (TCP/IP across machines)

No conditional compilation. No transport-specific code. Define the service interface once in .siml files; generated code handles serialization and routing automatically.

Configurable Flow Control: Set event queue limits per component. When queues fill:

  • Oldest events of same priority are displaced
  • Priority system prevents low-priority events from displacing high-priority events
  • Prevents memory exhaustion from producer/consumer rate mismatches

Broadcast/Pub-Sub: One response or event delivers to multiple subscribers simultaneously:

// Provider broadcasts
void TemperatureService::requestGetTemperature() {
    float temp = readSensor();
    responseGetTemperature(temp);  // All subscribers notified
}

// Multiple consumers receive
void Dashboard::responseGetTemperature(float temp) { updateDisplay(temp); }
void Logger::responseGetTemperature(float temp) { logValue(temp); }
void Alarm::responseGetTemperature(float temp) { checkThreshold(temp); }
Enter fullscreen mode Exit fullscreen mode

Combining Both: Complementary Strengths

Coroutines and event-driven frameworks address different concerns:

  • Coroutines: Local async control flow within a process
  • Event frameworks: Distributed messaging across processes/machines

They work together effectively:

// Areg service method using coroutines for internal async logic
void MyService::requestComplexOperation(const String& input) {
    // Local async processing with coroutines
    auto intermediate = co_await parseAndValidate(input);
    auto computed = co_await performCalculation(intermediate);
    auto verified = co_await verifyResult(computed);

    // Areg broadcasts result across network
    responseComplexOperation(verified);  // Automatic IPC/network routing
}
Enter fullscreen mode Exit fullscreen mode

Division of responsibility:

  • Coroutines: Sequential async logic, complex control flow, algorithmic processing
  • Areg: Threading management, IPC, network communication, service lifecycle

Use Case Analysis

When Coroutines Excel

Sequential operations with dependencies:
Each step requires the previous result. Coroutines maintain readable, maintainable code without manual state machines.

I/O-bound single-process servers:
Thousands of concurrent connections with minimal per-connection computation. Coroutines enable one thread to multiplex many operations.

Algorithmic async pipelines:
Complex async workflows within a single process where linear control flow clarifies logic.

When Areg Excels

Distributed service-oriented systems:
Multiple processes or machines providing and consuming services. Areg handles discovery, routing, serialization automatically.

Pub-sub architectures:
One-to-many communication patterns where events broadcast to multiple subscribers.

Multi-threaded applications requiring thread safety:
Thread affinity guarantees eliminate data races architecturally, not through defensive locking.

Systems requiring transport flexibility:
Develop locally (inter-thread), test with IPC, deploy distributed -- same codebase.

Fog/edge computing and IoT:
Devices dynamically discovering services, handling network failures, reconnecting automatically.


Trade-Off Analysis: What Each Cannot Do

Coroutines Limitations for Distribution

No broadcast: One co_await resumes one coroutine. Delivering results to multiple consumers requires additional infrastructure.

No thread affinity: Without custom executors, coroutines may resume on arbitrary threads, making thread-local storage unsafe.

No built-in IPC: Serialization, transport, routing, service discovery -- all manual implementation.

State machine distribution: Coroutine state lives in one process. Distributing stateful workflows across processes requires external coordination.

Event-Driven Limitations for Sequential Logic

State storage across callbacks: Multi-step operations require member variables to hold intermediate state:

class SequentialProcessor {
    String mStepA;  // Store between callbacks
    int mStepB;

    void startProcessing() {
        mProxy.requestStepA();
    }

    void responseStepA(const String& result) {
        mStepA = result;
        mProxy.requestStepB(mStepA);
    }

    void responseStepB(int result) {
        mStepB = result;
        mProxy.requestStepC(mStepA, mStepB);
    }
};
Enter fullscreen mode Exit fullscreen mode

Compare with coroutine's linear flow:

Task<void> sequentialProcessing() {
    auto stepA = co_await requestStepA();
    auto stepB = co_await requestStepB(stepA);
    auto stepC = co_await requestStepC(stepA, stepB);
}
Enter fullscreen mode Exit fullscreen mode

For complex local sequential logic, coroutines reduce code verbosity and improve maintainability.


Performance Considerations

Different Optimization Goals

Coroutines optimize for:
Local execution efficiency. Minimal per-operation overhead within a single process. Primary cost is initial frame allocation -- mitigated with custom allocators.

Areg optimizes for:
Distributed system productivity. Event dispatch includes serialization, thread-safe queuing, and cross-process routing overhead.

Critical insight: When services communicate over IPC or network, Areg's dispatch overhead is negligible compared to transport latency. Network calls measure in milliseconds; event dispatch in microseconds -- 0.1% of total time.

Areg competes with manually implementing distributed systems infrastructure (weeks of development, error-prone), not with local-only coroutines that provide no distribution support.

Resource Characteristics

Aspect Coroutines Areg Events
Allocation Heap-allocated frame Heap-allocated event object
Thread model No built-in affinity Strict thread affinity per component
Scalability Thousands of concurrent operations Thousands of concurrent services
Distribution Requires custom implementation Built-in IPC/network support

Both are lightweight enough for production systems. Choose based on architectural requirements, not raw performance numbers.


Decision Framework

Choose C++20 Coroutines When:

  • Building single-process async systems
  • Sequential operations with dependencies are common
  • Linear code readability improves maintainability for your team
  • I/O-bound workloads with many concurrent operations
  • You have expertise to build coroutine infrastructure (or use existing libraries)

Choose Areg SDK When:

  • Building distributed systems (multi-process or multi-machine)
  • IPC or network communication is fundamental
  • Pub-sub or broadcast patterns are core requirements
  • Thread safety without explicit locking is critical
  • Transport transparency enables flexible deployment
  • Automatic service discovery and reconnection matter
  • Development velocity matters -- batteries-included infrastructure

Use Both When:

  • Applications have local async logic (coroutines) AND distributed communication (Areg)
  • Coroutines handle complex sequential workflows within services
  • Areg handles inter-service communication, threading, and distribution
  • You want maximum expressiveness for different problem domains

Getting Started with Areg SDK

Repository: github.com/aregtech/areg-sdk

Quick Start:

git clone https://github.com/aregtech/areg-sdk.git
cd areg-sdk
cmake -B build
cmake --build build
Enter fullscreen mode Exit fullscreen mode

Explore Examples:
The examples/ directory contains working systems demonstrating:

  • Inter-thread, IPC, and network communication
  • Pub-sub patterns and broadcasts
  • Service discovery and automatic reconnection
  • Watchdog and fault tolerance
  • Distributed logging

Documentation: Areg SDK Wiki


Key Takeaways

  1. Different programming models: Coroutines provide sequential syntax. Event-driven provides callback-based async messaging.

  2. Different problem domains: Coroutines excel at local async operations. Event frameworks excel at distributed systems.

  3. Complementary, not competing: Use coroutines inside event-driven services for best of both worlds.

  4. Infrastructure investment: Coroutines require building custom infrastructure. Areg provides production-ready distributed systems infrastructure.

  5. Thread safety models: Coroutines need manual synchronization. Areg provides architectural thread affinity -- single-threaded component logic in multi-threaded systems.

  6. Distribution support: Coroutines have none built-in. Areg has IPC, network, service discovery, and fault tolerance integrated.

  7. Choose based on architecture: For local async, coroutines offer expressive syntax. For distributed systems, event-driven frameworks eliminate enormous complexity.

Understanding both approaches -- and when to use each -- makes you a more effective C++ systems architect.


Further Reading:


Have questions or feedback? Join the discussion on Areg SDK GitHub Issues.

Top comments (0)