start a coroutine immediately C++ asio
Asked Answered
E

1

6

When using pre-coroutine asio interface I could dispatch an async function in the current execution context (main function) and let it finish in the ctx.

#include <iostream>
#include <asio.hpp>
using namespace std;
using namespace literals;

int main() {
    asio::io_context ctx;
    asio::steady_timer t(ctx, 2s);
    asio::dispatch([&](){
        cout << "inside coro before wait" << endl;
        t.async_wait([](asio::error_code){
            cout << "inside coro after wait" << endl;
        });
    });
    cout << "starting ctx.run()" << endl;
    ctx.run();
}
$ ./sol
inside coro before wait
starting ctx.run()
inside coro after wait

I would like to replicate this with asio::awaitable but dispatch doesn't support coroutines.

On the other hand co_spawn only adds the coroutine to the context, and doesn't initiate it immediately.

I could use ctx.poll()/ctx.run_one() but the coroutine would have to be the only one ready to execute or at least the first one.

Anwser

If I'm not mistaken the only way to start a coroutine (from outside of coroutine) is using asio::co_spawn(executor, awaitable, token). In contrast to dispatch, co_spawn takes an executor as an argument, where as @sehe said dispatch uses associated executor. So to answer my question on how to start a coroutine immediately is to run it in asio::system_executor().

#include <bits/stdc++.h>
#include <asio.hpp>
using namespace std;
using namespace literals;

int main() {
    asio::io_context ctx;
    asio::co_spawn(asio::system_executor(), [&]() -> asio::awaitable<void> {
        cout << "inside coro before wait" << endl;
        asio::steady_timer t(ctx, 2s);
        co_await t.async_wait(asio::use_awaitable);
        cout << "inside coro after wait" << endl;
        co_return;
    }, asio::detached);
    cout << "starting ctx.run()" << endl;
    ctx.run();
}
$ ./sol
inside coro before wait
starting ctx.run()
inside coro after wait
Essential answered 21/3, 2023 at 18:42 Comment(0)
H
3

There's a myriad ways in which this code won't do what it suggests/you describe it will do:

asio::dispatch([](){
    // executes immediately
    asio::steady_timer t(ctx, 2s);
    t.async_wait([](){
        // executes in ctx.run()
    });
});
ctx.run();
  • It will never wait 2s, because t's destructor will cancel the async_await immediately after being initiated.

  • ctx is apparently a static or a global, because it isn't captured

  • the completion handler doesn't have the required signature

  • even if it did correctly accept an error_code variable, it would not "execute[s] in ctx.run()" because the dispatch dispatches on the associated executor of the lambda. This, because nothing else is associated, will default to a default-constructed instance of asio::system_executor.

    See e.g. Which io_context does std::boost::asio::post / dispatch use?

Q. On the other hand asio::co_spawn only adds the coroutine to the context, and doesn't initiate it immediately.

I don't think that's accurate either. As you mention the code you give is invalid (because post doesn't support coroutines). When fixed to use co_spawn, we can show it to run inside ctx.run():

#include <boost/asio.hpp>
#include <iostream>
namespace asio = boost::asio;
using namespace std::chrono_literals;

int main() {
    asio::io_context ctx;
    asio::co_spawn(
        ctx,
        []() -> asio::awaitable<void> {
            auto ex = co_await asio::this_coro::executor;
            asio::steady_timer t(ex, 2s);
            co_await t.async_wait(asio::use_awaitable);
            std::cout << "inside ctx.run()" << std::endl;
            // co_return;
        },
        asio::detached);

    std::cout << "before ctx.run()" << std::endl;
    ctx.run();
    std::cout << "after ctx.run()" << std::endl;
}

Prints

enter image description here

I really think 99% of the question hinges on these false premises, so I'll await your reaction before expounding.

UPDATE

Thinking longer I think you might be having a variation on this question: asio How to change the executor inside an awaitable?, but without realizing that you had unwittingly been using the system context for the dispatch.

Here's a more comprehensive illustration of precisely how (associated) executors interact with coroutine resume: full size

enter image description here

NOTE also how the work guard is required to avoid ctx running out of work while work is only pending on the system context.

I strongly recommend against relying on the system context, but this illustration should help you connect all the dots?

Hovel answered 22/3, 2023 at 2:2 Comment(3)
Thinking longer I think you might be having a variation on this question, but without realizing that you had unwittingly been using the system context for the dispatch.Hovel
Here's a more comprehensive illustration of precisely how (associated) executors interact with coroutine resume: imgur.com/a/cwCWq0n - note also how the work guard is required to avoid ctx running out of work while work is only pending on the system context. I strongly recommend against relying on the system context, but this illustration should help you connect all the dots?Hovel
What I actually wanted is to change the execution context of the awaitable and start it in the system_executor, thank you for clarifying thingsEssential

© 2022 - 2024 — McMap. All rights reserved.