c++redisboost-asioc++-coroutine

Can I call asio::co_spawn(ioc, ...) on separate thread after blocking call to ioc.run()


I'm new to boost asio, coroutines (and redis). I am prototyping a redis client using boost redis, which is built on top of asio and coroutines.

This question is more about asio and coroutines than boost redis, but for context, my redis client class will every-so-often receive calls to asynchronously write to redis. I am thinking that my class can hold an io_context (and the redis connection), and in its constructor will run the context in a new thread. A call to the write method will be implemented by calling co_spawn -- and will be executed by the context on the thread created in the constructor.

Update: thanks to sehe's answer and comments, I now realize that this question is specific to boost redis, because the call to ioc.run() blocks even after there are no more coroutines to execute. The run call returns when the boost redis connection (created using ioc) is cancelled.

My question is, is it legal to co_spawn another coroutine onto the io content after run has been called, on another thread? I have no idea, for example, whether the io context is thread safe, or whether there is some more subtle issue in this approach. A secondary question is whether this is a reasonable approach or whether there is an established design pattern for this problem. An alternative would be signaling the coroutine to wake up somehow, possibly using a timer as a signaling mechanism (e.g. like this). However I like the idea of spawning a new coroutine as it's simple and there is no need to manage state shared between threads.

I wrote a simple program to test whether the approach could work, adapting a boost redis example. While this works, again, I am a noob to asio / coroutines, and do not know whether it is sensible.

auto
co_ping(shared_ptr<connection> conn, string str) -> asio::awaitable<void>
{
    request req;
    req.push("PING", std::move(str));
    response<std::string> resp;
    co_await conn->async_exec(req, resp, asio::deferred_t{});
    SPDLOG_INFO("PING: {}", std::get<0>(resp).value());
}

int
main(int argc, char* argv[])
{
    config cfg;
    if (argc == 3) {
        cfg.addr.host = argv[1];
        cfg.addr.port = argv[2];
    }

    asio::io_context ioc;
    auto             conn = std::make_shared<connection>(ioc);
    conn->async_run(cfg, {}, asio::consign(asio::detached, conn));

    SPDLOG_INFO("Spawn 1 -- before event loop started");
    asio::co_spawn(ioc, co_ping(conn, "Hello 1"), asio::detached);

    auto fut = std::async(
        std::launch::async,
        [&ioc]() {
            SPDLOG_INFO("ioc run start");
            ioc.run(); // start event loop
            SPDLOG_INFO("ioc run end");
        });
    SPDLOG_INFO("Wait for ioc run on secondary thread");
    std::this_thread::sleep_for(std::chrono::milliseconds{100});

    SPDLOG_INFO("Spawn 2 -- after event loop started");
    asio::co_spawn(ioc, co_ping(conn, "Hello 2"), asio::detached);

    SPDLOG_INFO("Wait for pings");
    std::this_thread::sleep_for(std::chrono::milliseconds{500});
    SPDLOG_INFO("Cancel");
    conn->cancel();

    return 0;
}

output, noting [console] / [ioc] shows main / ioc thread running

[2025-06-11T16:01:03.278+01:00] [main.cpp:61] [main] [console] [info] Spawn 1 -- before event loop started
[2025-06-11T16:01:03.279+01:00] [main.cpp:73] [main] [console] [info] Wait for ioc run on secondary thread
[2025-06-11T16:01:03.279+01:00] [main.cpp:69] [main::<lambda_1>::operator ()] [ioc] [info] ioc run start
[2025-06-11T16:01:03.281+01:00] [main.cpp:38] [co_ping] [ioc] [info] PING: Hello 1
[2025-06-11T16:01:03.395+01:00] [main.cpp:76] [main] [console] [info] Spawn 2 -- after event loop started
[2025-06-11T16:01:03.395+01:00] [main.cpp:86] [main] [console] [info] Wait for pings
[2025-06-11T16:01:03.396+01:00] [main.cpp:38] [co_ping] [ioc] [info] PING: Hello 2
[2025-06-11T16:01:03.902+01:00] [main.cpp:88] [main] [console] [info] Cancel
[2025-06-11T16:01:03.904+01:00] [main.cpp:71] [main::<lambda_1>::operator ()] [ioc] [info] ioc run end

Solution

  • After run() returns, you need to restart() before running again. This is documented:

    A normal exit from the run() function implies that the io_context object is stopped (the stopped() function returns true). Subsequent calls to run(), run_one(), poll() or poll_one() will return immediately unless there is a prior call to restart().

    Note that you can use a work-guard to avoid running out of work (keeping the thread alive), or instead use asio::threadpool(1) to get exactly the same behavior.

    Update

    In response to the comments, here's with io_context:

    #include <boost/asio.hpp>
    #include <boost/redis/connection.hpp>
    #include <spdlog/spdlog.h>
    
    namespace asio = boost::asio;
    using boost::redis::config;
    using boost::redis::connection;
    using boost::redis::request;
    using boost::redis::response;
    
    asio::awaitable<void> co_ping(std::shared_ptr<connection> conn, std::string str) {
        SPDLOG_INFO("PING: {}", str);
        request req;
        req.push("PING", std::move(str));
        response<std::string> resp;
        co_await conn->async_exec(req, resp/*, asio::deferred*/);
        SPDLOG_INFO(" --> PING response: {}", std::get<0>(resp).value());
    }
    
    int main(int argc, char* argv[]) {
        using std::this_thread::sleep_for;
        using namespace std::chrono_literals;
    
        config cfg;
        if (argc == 3) {
            cfg.addr.host = argv[1];
            cfg.addr.port = argv[2];
        }
    
        asio::io_context ioc;
        auto             conn = std::make_shared<connection>(ioc);
        conn->async_run(cfg, {}, asio::consign(asio::detached, conn));
    
        SPDLOG_INFO("Spawn 1 -- before event loop started");
        co_spawn(ioc, co_ping(conn, "Hello 1"), asio::detached);
    
        std::thread th([&ioc] {
            // kLog::taskName = "ioc";
            SPDLOG_INFO("ioc run START");
            ioc.run(); // start event loop
            SPDLOG_INFO("ioc run EXIT");
        });
        sleep_for(100ms);
    
        SPDLOG_INFO("Spawn 2");
        co_spawn(ioc, co_ping(conn, "Hello 2"), asio::detached);
    
        SPDLOG_INFO("Wait for pings");
        sleep_for(500ms);
        SPDLOG_INFO("Cancel");
    
        post(ioc, [conn] { conn->cancel(); });
        th.join();
    }
    
    #include <boost/redis/src.hpp>
    

    enter image description here

    With thread_pool instead

    Even though there was no need for a work guard (due to async_run) you can still simplify using thread_pool:

        asio::thread_pool ioc{1};
        auto              conn = std::make_shared<connection>(ioc.get_executor());
        conn->async_run(cfg, {}, consign(asio::detached, conn));
    
        /// unchanged...
    
        ioc.join();