Home > other >  Boost asio C 20 Coroutines: co_spawn a coroutine with a by-reference parameter unexpected result
Boost asio C 20 Coroutines: co_spawn a coroutine with a by-reference parameter unexpected result

Time:08-25

In the following code, the parameter of the session coroutine is passed by reference.

#include <boost/asio.hpp>
#include <iostream>

boost::asio::awaitable<void> session(const std::string& name)
{
    std::cout << "Starting " << name << std::endl;
    auto executor = co_await boost::asio::this_coro::executor;
}

int main()
{
    boost::asio::io_context io_context;

    co_spawn(io_context, session("ServerA"), boost::asio::detached);
    co_spawn(io_context, session("ServerB"), boost::asio::detached);

    io_context.run();

    return 0;
}

For some reason that I don't understand, the above code results in printing Starting ServerB twice.

> g   -std=c  20 ../test-coro.cpp -o test-coro && ./test-coro
Starting ServerB
Starting ServerB

But when I change the coroutine parameter to pass by value, it will correctly print both Starting ServerA and Starting ServerB

#include <boost/asio.hpp>
#include <iostream>

boost::asio::awaitable<void> session(std::string name)
{
    std::cout << "Starting " << name << std::endl;
    auto executor = co_await boost::asio::this_coro::executor;
}

int main()
{
    boost::asio::io_context io_context;

    co_spawn(io_context, session("ServerA"), boost::asio::detached);
    co_spawn(io_context, session("ServerB"), boost::asio::detached);

    io_context.run();

    return 0;
}
> g   -std=c  20 ../test-coro.cpp -o test-coro && ./test-coro
Starting ServerA
Starting ServerB

Is it expected or this is a compiler/library bug? If it is expected, then what's the reasoning for it?

Environment:
Arch Linux 5.18.16-arch1-1
gcc (GCC) 12.2.0
boost version 1.79

CodePudding user response:

You can think of the coroutine state as containing what would be on the function call stack (which is what makes the function resumable): cppreference

When a coroutine begins execution, it performs the following:

  • allocates the coroutine state object using operator new (see below)
  • copies all function parameters to the coroutine state: by-value parameters are moved or copied, by-reference parameters remain references (and so may become dangling if the coroutine is resumed after the lifetime of referred object ends)

The coroutine logically stores a reference to a temporary string. Oops.

I haven't checked, but I'd assume that Asio's awaitable implementation starts with an initial suspend_always (this makes intuitive sense to me with respect to the executor model).

Yes, this means that co_spawn with any reference argument means the lifetime of the object referenced must be guaranteed.

On my system, the output was merely

Starting
Starting

One fix is what you showed. To illustrate out the lifetime aspect:

{
    std::string a="ServerA", b="ServerB";
    co_spawn(io_context, session(a), boost::asio::detached);
    co_spawn(io_context, session(b), boost::asio::detached);

    io_context.run();
}

Is also a valid fix. For the trivial example I'd suggest a std::string_view regardless:

Live On Coliru

#include <boost/asio.hpp>
#include <iomanip>
#include <iostream>

boost::asio::awaitable<void> session(std::string_view name)
{
    std::cout << "Starting " << std::quoted(name) << std::endl;
    auto executor = co_await boost::asio::this_coro::executor;
}

int main()
{
    boost::asio::io_context io_context;

    std::string const a="ServerA";
    co_spawn(io_context, session(a), boost::asio::detached);
    co_spawn(io_context, session("ServerB"), boost::asio::detached);

    io_context.run();
}

Printing

Starting "ServerA"
Starting "ServerB"
  • Related