c++boost-asioc++20coroutineasio

asio::steady_timer does NOT work with completion handler "use_awaitable"


I am being confused the asio::timer coroutine with completion handler "use_awaitable" does not work if I modify the timer in another coroutine.

The "Leave foo" never reached:

Enter foo
Enter bar
Leave bar
The End

Below is code:

static asio::awaitable<void> foo(asio::steady_timer& timer)
{
    cout << "Enter foo" << endl;
    timer.expires_from_now(asio::steady_timer::clock_type::duration::max());
    co_await timer.async_wait(asio::use_awaitable);
    cout << "Leave foo" << endl;
}

static asio::awaitable<void> bar(asio::steady_timer& timer)
{
    cout << "Enter bar" << endl;
    sleep(2); // wait a little for asio::io_service::run to be executed
    timer.expires_after(asio::chrono::seconds(1));
    cout << "Leave bar" << endl;
    co_return;
}

int main()
{
    asio::io_context ioService;
    asio::steady_timer timer(ioService);

    asio::co_spawn(ioService, foo(timer), asio::detached);
    asio::co_spawn(ioService, bar(timer), asio::detached);

    ioService.run();
    std::printf("The End\n");
    return 0;
}

Actually I just want to suspend a coroutine and resume it in another coroutine over asio/c++20 without any other threads.


Solution

  • The docs state that expires_after cancels any pending async wait. Using BOOST_ASIO_ENABLE_HANDLER_TRACKING:

    enter image description here

    You don't handle the exception. If you did, you would notice it happening:

    static asio::awaitable<void> foo(asio::steady_timer& timer) try {
        std::cout << "Enter foo" << std::endl;
        timer.expires_from_now(asio::steady_timer::clock_type::duration::max());
        co_await timer.async_wait(asio::use_awaitable);
        std::cout << "Leave foo" << std::endl;
    } catch (boost::system::system_error const& se) {
        std::cout << "Error: " << se.what() << std::endl;
    }
    

    Now the output is

    Enter foo
    Enter bar
    Leave bar
    Error: Operation canceled [system:125]
    The End
    

    Alternatively you might use error-codes:

    boost::system::error_code ec;
    co_await timer.async_wait(redirect_error(asio::use_awaitable, ec));
    std::cout << "Leave foo (" << ec.message() << ")" << std::endl;
    

    Or even:

    auto [ec] = co_await timer.async_wait(as_tuple(asio::use_awaitable));
    std::cout << "Leave foo (" << ec.message() << ")" << std::endl;
    

    Both printing (Live):

    Enter foo
    Enter bar
    Leave bar
    Leave foo (Operation canceled)
    The End
    

    More Problems / Ideas

    There's a problem with sleep-ing in a coro. It block the execution context, so no asynchronous work will be able to complete. That's certainly not what you want.

    Besides, it looks a lot like you are trying to coordinate execution of two coroutines. If you want "signal-like" behaviour, then by all means, this can work as long as you correctly handle the error-codes as well.

    If you just want to coordinate cancellation, use the cancellation_slot facility. In fact, there exists pretty powerful syntactic sugar for binding shared cancellation slots across asio::awaitable<> instances:

    Live On Coliru

    #include <boost/asio.hpp>
    #include <boost/asio/experimental/awaitable_operators.hpp>
    #include <iomanip>
    #include <iostream>
    namespace asio = boost::asio;
    using namespace std::chrono_literals;
    using namespace asio::experimental::awaitable_operators;
    
    static void trace(char const* msg) {
        static constexpr auto now = std::chrono::steady_clock::now;
        static auto const start = now();
        std::cout << std::setw(4) << (now() - start) / 1ms << "ms " << msg << std::endl;
    }
    
    static asio::awaitable<void> async_simulate_work(auto delay) {
        co_await asio::steady_timer(co_await asio::this_coro::executor, delay)
            .async_wait(asio::use_awaitable);
    }
    
    static asio::awaitable<void> foo() try {
        trace("Foo enter");
        for (;;) {
            trace("Foo working...");
            co_await async_simulate_work(100ms);
        }
        trace("Foo leave");
    } catch (boost::system::system_error const& se) {
        trace("Foo cancel");
    }
    
    static asio::awaitable<void> bar() {
        trace("Bar enter");
        co_await async_simulate_work(260ms);
        trace("Bar leave");
    }
    
    int main() {
        asio::io_context   ioService;
        asio::steady_timer timer(ioService);
    
        asio::co_spawn(ioService, foo() || bar(), asio::detached);
    
        ioService.run();
        trace("The End");
    }
    

    Printing e.g.

        g++ -std=c++20 -O2 -Wall -pedantic -pthread main.cpp && ./a.out
    g++ -std=c++20 -O2 -Wall -pedantic -pthread main.cpp && ./a.out
       0ms Foo enter
       0ms Foo working...
       0ms Bar enter
     100ms Foo working...
     200ms Foo working...
     260ms Bar leave
     260ms Foo cancel
     260ms The End