Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASIO strand blocked after co_spawn is co_await-ed

I've been experimenting with ASIO strands and C++20 coroutines, and noticed weird behaviour. When I spawn a coroutine on one strand, and inside it spawn another coroutine on a second strand, and await on it, when the control is returned to the outer coroutine, both strands are blocked. Is this intended behaviour? If so, how do I work around this?

Following code demonstrates the issue, tested with boost.asio, boost version 1.74.0, gcc version 10.2.0 on linux.

#include <thread>
#include <chrono>
#include <iostream>

#include <boost/asio.hpp>

namespace asio = boost::asio;
using namespace std::chrono_literals;

int main() {
    auto ioc = asio::io_context{};
    // Create two strands
    auto s1 = asio::make_strand(ioc);
    auto s2 = asio::make_strand(ioc);
    // Prevent the io context from stopping
    auto wg = asio::make_work_guard(ioc);
    // Over-provision threads just in case
    auto w1 = std::jthread{[&]() { ioc.run(); }};
    auto w2 = std::jthread{[&]() { ioc.run(); }};
    auto w3 = std::jthread{[&]() { ioc.run(); }};
    auto w4 = std::jthread{[&]() { ioc.run(); }};
    auto w5 = std::jthread{[&]() { ioc.run(); }};

    asio::co_spawn(s1, [&]() -> asio::awaitable<void> {
        std::cout << "a" << std::endl;
        co_await asio::co_spawn(s2, []() -> asio::awaitable<void> {
            // Works as intended
            // s2 is blocked, s1 is free (b will print)
            std::this_thread::sleep_for(2s);
            co_return;
        }, asio::use_awaitable);
        // Expected: s1 is blocked, s2 is free (d will print, e will wait 5s)
        // Reality: both s1 and s2 are blocked (after 5s, d and e print at the same time).
        // Why?
        std::cout << "c" << std::endl;
        std::this_thread::sleep_for(5s);
    }, asio::detached);

    std::this_thread::sleep_for(1s);

    asio::co_spawn(s1, []() -> asio::awaitable<void> {
        std::cout << "b" << std::endl;
        co_return;
    }, asio::detached);

    std::this_thread::sleep_for(2s);

    asio::co_spawn(s2, []() -> asio::awaitable<void> {
        std::cout << "d" << std::endl;
        co_return;
    }, asio::detached);

    asio::co_spawn(s1, []() -> asio::awaitable<void> {
        std::cout << "e" << std::endl;
        co_return;
    }, asio::detached);

    std::this_thread::sleep_for(10s);
    ioc.stop();
}

Expected behaviour:

t__0_1_2_3_4_5_6_7
s1 a_b_c_________e
s2 ______d________

Actual behaviour:

t__0_1_2_3_4_5_6_7
s1 a_b_c_________e
s2 ______________d
like image 852
Michal Jankovic Avatar asked Aug 31 '25 03:08

Michal Jankovic


1 Answers

Figured out a solution: instead of spawning the coroutine on the strand, spawn it on the io context, and post to a strand when access through that strand is required.

asio::co_spawn(ioc, [&]() -> asio::awaitable<void> {
        co_await asio::post(s1, asio::use_awaitable);

        std::cout << "a" << std::endl;

        co_await asio::post(s2, asio::use_awaitable);

        std::this_thread::sleep_for(2s);

        co_await asio::post(s1, asio::use_awaitable);

        std::cout << "c" << std::endl;
        std::this_thread::sleep_for(5s);
    }, asio::detached);
like image 190
Michal Jankovic Avatar answered Sep 02 '25 15:09

Michal Jankovic