Canopy supports both blocking and coroutine execution modes using the same codebase. This section explains how bi-modal execution works and when to use each mode.
Canopy uses preprocessor macros to switch between blocking and coroutine modes:
// From coroutine_support.h
#ifdef CANOPY_BUILD_COROUTINE
#define CORO_TASK(x) coro::task<x>
#define CO_RETURN co_return
#define CO_AWAIT co_await
#define SYNC_WAIT(x) coro::sync_wait(x)
#else
#define CORO_TASK(x) x
#define CO_RETURN return
#define CO_AWAIT
#define SYNC_WAIT(x) x
#endifBlocking Mode (CANOPY_BUILD_COROUTINE=OFF):
CORO_TASK(error_code) my_method(int value)
{
int result;
CO_AWAIT proxy_->call(value, result);
CO_RETURN error::OK();
}
// Transforms to:
error_code my_method(int value)
{
int result;
proxy_->call(value, result);
return error::OK();
}Coroutine Mode (CANOPY_BUILD_COROUTINE=ON):
CORO_TASK(error_code) my_method(int value)
{
int result;
CO_AWAIT proxy_->call(value, result);
CO_RETURN error::OK();
}
// Transforms to:
coro::task<error_code> my_method(int value)
{
int result;
co_await proxy_->call(value, result);
co_return error::OK();
}- Blocking mode is easier to debug (no coroutine complexity)
- Standard debuggers work without special coroutine support
- Simpler stack traces
- Coroutine mode enables efficient async I/O
- No thread blocking during network calls
- Better resource utilization
- Single source code for both modes
- No code duplication
- Consistent behavior
// Always use macros in interface implementations
CORO_TASK(error_code) my_service::do_work(int input, [out] int& output)
{
// This works in both modes
auto error = CO_AWAIT worker_->process(input, output);
if (error != error::OK())
{
CO_RETURN error;
}
CO_RETURN error::OK();
}At Build Time:
# Blocking mode (default)
cmake --preset Debug
# Coroutine mode
cmake --preset Debug_CoroutineFor synchronous code (runs in both modes):
// Works identically in both modes
auto result = calculate_sync();For asynchronous code (requires coroutines):
#ifdef CANOPY_BUILD_COROUTINE
// Coroutine-specific code
auto task = async_operation();
CO_AWAIT task;
#else
// Blocking fallback
auto result = blocking_operation();
#endifFor coroutine mode, set up an IO scheduler:
auto scheduler = coro::scheduler::make_unique(
coro::scheduler::options{
.thread_strategy = coro::scheduler::thread_strategy_t::spawn,
.pool = coro::thread_pool::options{
.thread_count = std::thread::hardware_concurrency(),
},
.execution_strategy = coro::scheduler::execution_strategy_t::process_tasks_on_thread_pool
});
// Pass scheduler to service
auto service = std::make_shared<rpc::service>(
"my_service",
rpc::zone{1},
scheduler);// Spawn a coroutine task
scheduler->spawn([&]() -> CORO_TASK(void)
{
int result;
auto error = CO_AWAIT calculator_->add(10, 20, result);
std::cout << "Result: " << result << "\n";
CO_RETURN;
}());
// Process events until complete
bool done = false;
while (!done)
{
scheduler->process_events(std::chrono::milliseconds(1));
}- Use
CORO_TASK,CO_RETURN,CO_AWAITconsistently - Keep coroutines short and focused
- Handle errors at each await point
- Use blocking mode for debugging
- Mix blocking calls with coroutines
- Block in coroutine mode without careful consideration
- Assume synchronous behavior in coroutine mode
- Thread per connection model
- Simpler mental model
- Higher memory usage
- Good for CPU-bound work
- Event-driven model
- Thousands of concurrent operations
- Lower memory footprint
- Better for I/O-bound work
- Add scheduler to service constructor
- Replace
returnwithCO_RETURN - Add
CO_AWAITbefore async operations - Spawn tasks instead of calling directly
- Process events in main loop
// Blocking version
error_code blocking_work()
{
return calculator_->add(1, 2, result);
}
// Coroutine version
CORO_TASK(error_code) async_work()
{
CO_RETURN CO_AWAIT calculator_->add(1, 2, result);
}
// Usage in coroutine mode
scheduler->spawn([&]() -> CORO_TASK(void)
{
auto error = CO_AWAIT async_work();
if (error == rpc::error::OK())
{
std::cout << "Success!\n";
}
CO_RETURN;
}());CORO_TASK(void) sequential_work()
{
// Run operations one after the other
auto task1 = calculator_->add(1, 2, result1);
auto task2 = calculator_->multiply(3, 4, result2);
// Await each in turn
CO_AWAIT task1;
CO_AWAIT task2;
CO_RETURN;
}CORO_TASK(error_code) chained_operations()
{
int result;
// Each error propagates up
auto error = CO_AWAIT calculator_->add(10, 20, result);
if (error != rpc::error::OK())
{
CO_RETURN error;
}
error = CO_AWAIT calculator_->multiply(result, 2, result);
if (error != rpc::error::OK())
{
CO_RETURN error;
}
CO_RETURN rpc::error::OK();
}- YAS Serializer - Learn about encoding formats
- Protocol Buffers - Cross-language serialization
- Error Handling - Understand error propagation
- Telemetry - Debug coroutine execution