Libcoro
C++20 coroutine library
Install / Use
/learn @jbaldwin/LibcoroREADME
libcoro C++20 coroutine library
[![language][badge.language]][language]
[![license][badge.license]][license]
libcoro is licensed under the Apache 2.0 license.
libcoro is meant to provide low level coroutine constructs for building larger applications.
Overview
- C++20 coroutines!
- Modern Safe C++20 API
- Higher level coroutine constructs
- coro::sync_wait(awaitable)
- coro::when_all(awaitable...) -> awaitable
- coro::when_any(awaitable...) -> awaitable
- coro::task<T>
- coro::generator<T>
- coro::event
- coro::latch
- coro::mutex
- coro::shared_mutex
- coro::semaphore
- coro::ring_buffer<element, num_elements>
- coro::queue
- coro::condition_variable
- coro::invoke(functor, args...) -> awaitable
- Executors
- coro::thread_pool for coroutine cooperative multitasking
- coro::scheduler for driving i/o events
- Can use
coro::thread_poolfor latency sensitive or long-lived tasks. - Can use inline task processing for thread per core or short-lived tasks.
- Requires
LIBCORO_FEATURE_NETWORKINGto be supported.
- Can use
- coro::task_group for grouping dynamic lifetime tasks
- Coroutine Networking
- coro::net::dns::resolver for async dns
- Uses libc-ares
- coro::net::tcp::client
- coro::net::tcp::server
- coro::net::tls::client (OpenSSL)
- coro::net::tls::server (OpenSSL)
- coro::net::udp::peer
- coro::net::dns::resolver for async dns
- Requirements
- Build Instructions
- Android Support
- Contributing
- Support
Usage
A note on co_await and threads
It's important to note with coroutines that any co_await has the potential to switch the underlying thread that is executing the currently executing coroutine if the scheduler used has more than 1 thread. In general this shouldn't affect the way any user of the library would write code except for thread_local. Usage of thread_local should be extremely careful and never used across any co_await boundary do to thread switching and work stealing on libcoro's schedulers. The only way this is safe is by using a coro::thread_pool with 1 thread or an inline scheduler which also only has 1 thread.
A note on lambda captures
C++ Core Guidelines - CP.51: Do no use capturing lambdas that are coroutines
The recommendation is to not use lambda captures and instead pass any data into the coroutine via its function arguments by value to guarantee the argument lifetimes. Lambda captures will be destroyed at the coroutines first suspension point so if they are used past that point it will result in a use after free bug.
If you must use lambda captures with your coroutines then libcoro offers coro::invoke to create a stable coroutine frame to hold the captures for the duration of the user's coroutine.
sync_wait
The sync_wait construct is meant to be used outside a coroutine context to block the calling thread until the coroutine has completed. The coroutine can be executed on the calling thread or scheduled on one of libcoro's schedulers.
#include <coro/coro.hpp>
#include <iostream>
int main()
{
// This lambda will create a coro::task that returns a unit64_t.
// It can be invoked many times with different arguments.
auto make_task_inline = [](uint64_t x) -> coro::task<uint64_t> { co_return x + x; };
// This will block the calling thread until the created task completes.
// Since this task isn't scheduled on any coro::thread_pool or coro::scheduler
// it will execute directly on the calling thread.
auto result = coro::sync_wait(make_task_inline(5));
std::cout << "Inline Result = " << result << "\n";
// We'll make a 1 thread coro::thread_pool to demonstrate offloading the task's
// execution to another thread. We'll pass the thread pool as a parameter so
// the task can be scheduled.
// Note that you will need to guarantee the thread pool outlives the coroutine.
auto tp = coro::thread_pool::make_unique(coro::thread_pool::options{.thread_count = 1});
auto make_task_offload = [](std::unique_ptr<coro::thread_pool>& tp, uint64_t x) -> coro::task<uint64_t>
{
co_await tp->schedule(); // Schedules execution on the thread pool.
co_return x + x; // This will execute on the thread pool.
};
// This will still block the calling thread, but it will now offload to the
// coro::thread_pool since the coroutine task is immediately scheduled.
result = coro::sync_wait(make_task_offload(tp, 10));
std::cout << "Offload Result = " << result << "\n";
}
Expected output:
$ ./examples/coro_sync_wait
Inline Result = 10
Offload Result = 20
when_all
The when_all construct can be used within coroutines to await a set of tasks, or it can be used outside coroutine context in conjunction with sync_wait to await multiple tasks. Each task passed into when_all will initially be executed serially by the calling thread so it is recommended to offload the tasks onto an executor like coro::thread_pool or coro::scheduler so they can execute in parallel.
#include <coro/coro.hpp>
#include <iostream>
int main()
{
// Create a thread pool to execute all the tasks in parallel.
auto tp = coro::thread_pool::make_unique(coro::thread_pool::options{.thread_count = 4});
// Create the task we want to invoke multiple times and execute in parallel on the thread pool.
auto twice = [](std::unique_ptr<coro::thread_pool>& tp, uint64_t x) -> coro::task<uint64_t>
{
co_await tp->schedule(); // Schedule onto the thread pool.
co_return x + x; // Executed on the thread pool.
};
// Make our tasks to execute, tasks can be passed in via a std::ranges::range type or var args.
std::vector<coro::task<uint64_t>> tasks{};
for (std::size_t i = 0; i < 5; ++i)
{
tasks.emplace_back(twice(tp, i + 1));
}
// Synchronously wait on this thread for the thread pool to finish executing all the tasks in parallel.
auto results = coro::sync_wait(coro::when_all(std::move(tasks)));
for (auto& result : results)
{
// If your task can throw calling return_value() will either return the result or re-throw the exception.
try
{
std::cout << result.return_value() << "\n";
}
catch (const std::exception& e)
{
std::cerr << e.what() << '\n';
}
}
// Use var args instead of a container as input to coro::when_all.
auto square = [](std::unique_ptr<coro::thread_pool>& tp, double x) -> coro::task<double>
{
co_await tp->schedule();
co_return x* x;
};
// Var args allows you to pass in tasks with different return types and returns
// the result as a std::tuple.
auto tuple_results = coro::sync_wait(coro::when_all(square(tp, 1.1), twice(tp, 10)));
auto first = std::get<0>(tuple_results).return_value();
auto second = std::get<1>(tuple_results).return_value();
std::cout << "first: " << first << " second: " << second << "\n";
}
Expected output:
$ ./examples/coro_when_all
2
4
6
8
10
first: 1.21 second: 20
when_any
The when_any construct can be used within coroutines to await a set of tasks and only return the result of the first task that completes. This can also be used outside of a coroutine context in conjunction with sync_wait to await the first result. Each task passed into when_any will initially be executed serially by the calling thread so it is recommended to offload the tasks onto an executor like coro::thread_pool or coro::scheduler so they can execute in parallel.
#include <coro/coro.hpp>
#include <iostream>
int main()
{
// Create a scheduler to execute all tasks in parallel and also so we can
// suspend a task to act like a timeout event.
auto scheduler = coro::scheduler::make_unique();
// This task will behave like a long running task and will produce a valid result.
auto make_long_running_task = [](std::unique_ptr<coro::scheduler>& scheduler,
std::chrono::milliseconds execution_time) -> coro::task<int64_t>
{
// Schedule the task to execute in parallel.
co_await scheduler->schedule();
// Fake doing some work...
co_await scheduler->yield_for(execution_time);
// Return the result.
co_return 1;
};
auto make_timeout_task = [](std::unique_ptr<coro::scheduler>& scheduler) -> coro::task<int64_t>
{
// Schedule a timer to be fired so we know the task timed out.
co_await scheduler->schedule_after(std::chrono::milliseconds{100});
co_return -1;
};
// Example showing the long running task completing first.
{
std::vector<coro::task<int64_t>> tasks{};
tasks.emplace_back(make_long_running_task(scheduler, std::chrono::milliseconds{50}));
