Search code examples
c++boostboost-asiocoroutine

Boost Asio: Implementing "events"?


Using Boost Asio, how would I implement an "event" class to resume C++20 coroutines?

Kind of like this:

// Oversimplicated, but hopefully good enough as example
struct oneshot_event {
    void raise();
    async::awaitable<void> wait();
};

Where wait() can be co_awaited and the coroutine is resumed as soon as something calls raise().

I know that Boost Asio has (or had? I found it in outdated documentation) a coro class that can yield, but that won't help me here, as there are multiple events that can be awaited. I am also not sure how all this Boost.asio and Boost.coroutine stuff works together

Tuxifan


Solution

  • The traditional answer would have employed an awaitable timer (gera posted an answer around that).

    However, that can be error prone and clumsy, especially with multithreading (both race conditions (e.g. https://stackoverflow.com/a/22204127/85371 and more specifically https://stackoverflow.com/a/43169596/85371) and data races (e.g. gera's code isn't threadsafe).

    A lot more flexible is the experimental channels support: https://www.boost.org/doc/libs/master/doc/html/boost_asio/overview/channels.html

    This will allow you to either use multiple channels for multiple events, or support many events over a single channel. Channels exist in thread-safe variant (asio::concurrent_channel) out of the box.

    Channel Demo

    As per the comment request, here's a sample based on channels:

    Live On Coliru

    #include <boost/asio.hpp>
    #include <boost/asio/co_spawn.hpp>
    #include <boost/asio/detached.hpp>
    #include <boost/asio/experimental/concurrent_channel.hpp>
    #include <iostream>
    #include <memory>
    
    namespace asio = boost::asio;
    using boost::system::error_code;
    using Channel = asio::experimental::concurrent_channel<void(error_code, std::size_t)>;
    using CoChannel = asio::use_awaitable_t<>::as_default_on_t<Channel>;
    
    asio::awaitable<void> myCoroutine(CoChannel& event) {
        try {
            std::cout << "Coroutine: waiting..." << std::endl;
            while (true) {
                auto evt = co_await event.async_receive();
                std::cout << "Coroutine: resumed! (" << evt << ")" << std::endl;
            }
        } catch (boost::system::system_error const& e) {
            std::cout << "Coroutine: " << e.code().message() << std::endl;
        }
        std::cout << "Coroutine: done" << std::endl;
    }
    
    int main() {
        asio::thread_pool io;
    
        CoChannel event(io);
        co_spawn(io, myCoroutine(event), asio::detached);
    
        for (auto i : {1, 2, 3}) {
            std::cout << "Main: Sleeping for " << i << " seconds..." << std::endl;
            std::this_thread::sleep_for(std::chrono::seconds(i));
    
            event.try_send(error_code{}, i);
            ; // Resume the coroutine
        }
    
        event.close();
        io.join();
        std::cout << "Main: done" << std::endl;
    }
    

    Testing locally (for visible timing):

    enter image description here