Yielding in Boost.Asio Stackful Coroutine
Asked Answered
M

3

6

When using Boost.Asio stackful coroutines, how can I "manually" yield so that another coroutine or async operation has a chance to run? For example, I need to perform a long computation before sending a response to a command I received from a TCP socket:

asio::spawn(strand_, [this, self](asio::yield_context yield)
{
    char data[256];
    while (socket_.is_open())
    {
        size_t n = socket_.async_read_some(boost::asio::buffer(data),
                                           yield);

        if (startsWith(data, "computePi"))
        {
            while (!computationFinished)
            {
                computeSomeMore();
                yield; // WHAT SHOULD THIS LINE BE?
            }

            storeResultIn(data);
            boost::asio::async_write(socket_, boost::asio::buffer(data, n),
                                     yield);
        }
    }
});
Millennium answered 30/9, 2014 at 18:17 Comment(0)
M
0

As of Boost 1.80, Jamboree's answer no longer works. What works for me is this:

boost::asio::post(boost::asio::get_associated_executor(yield), yield);
Millennium answered 18/8, 2022 at 21:7 Comment(0)
S
5

It's simpler than you think:

iosvc.post(yield);

will do the trick.

(iosvc borrowed from @sehe's sample code)

Steelworks answered 1/10, 2014 at 2:56 Comment(2)
This seems cleaner indeed.Polyhedron
Was good while it lasted, but no longer works in Boost 1.80. See my answer.Millennium
P
4

You can just call poll_one() on the io_service object.

Full working sample:

#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/thread.hpp>
#include <iostream>

namespace asio = boost::asio;
using boost::asio::ip::tcp;
using std::begin;
using std::end;

bool computationFinished = false;
void computeSomeMore() {
    static int count = 0;
    if (count++>10)
    {
        computationFinished = true;
        std::cout << "Calculation finished\n";
    } else
    {
        std::cout << "Calculating...\n";
        boost::this_thread::sleep_for(boost::chrono::milliseconds(200));
    }
}

template <typename T> void storeResultIn(T& a) {
    std::fill(begin(a), end(a), '4');
}

int main()
{
    asio::io_service iosvc;
    tcp::socket s(iosvc);
    tcp::resolver r(iosvc);

    tcp::acceptor a(iosvc, tcp::endpoint(tcp::v4(), 6767));

    a.accept(s);
    {
        asio::spawn(iosvc, [&iosvc,&s](asio::yield_context yield)
        {
            char data[256];
            while (s.is_open())
            {
                size_t n = s.async_read_some(boost::asio::buffer(data), yield);

                if (boost::algorithm::starts_with(data, "computePi"))
                {
                    iosvc.post([]{std::cout << "I can still breath\n";}); // some demo work
                    iosvc.post([]{std::cout << "And be responsive\n";});

                    while (!computationFinished)
                    {
                        computeSomeMore();
                        iosvc.poll_one(); // this enables the demo work to be run
                    }

                    storeResultIn(data);
                    boost::asio::async_write(s, boost::asio::buffer(data, n), yield);
                } else
                {
                    std::cout << "Received unknown command '" << std::string(data, data+n) << "'\n";
                }
            }
        });
    }

    iosvc.run();
    std::cout << "Bye bye\n";
}

When sent "computePi", server prints:

Calculating...
I can still breath
Calculating...
And be responsive
Calculating...
Calculating...
Calculating...
Calculating...
Calculating...
Calculating...
Calculating...
Calculating...
Calculating...
Calculation finished
Polyhedron answered 30/9, 2014 at 21:31 Comment(9)
I like how you turned my startsWith and storeResultIn pseudocode into actual working code!Millennium
It's an indispensible skill. I require myself to test my answers when possible. This is great exercise and quite fun to do.Polyhedron
Would iosvc.poll() instead of iosvc.poll_one() make the rest of the async tasks more responsive while the long computation is in progress?Millennium
Yeah, I used to write working examples too back when I was racking up reputation. I learned a great deal from that exercise.Millennium
@EmileCormier Of course. That (poll() vs. poll_one()) would only make sense if the calculation task is low prio, though. Also, note I chose poll_one() because it comes closest to the notion of "cooperatively yielding a thread" (cf. std::this_thread::yield)Polyhedron
If one watches out for infinite recursion, is it actually safe to call iosvc.poll* within an async handler, seeing that the async handler is being called within the context of iosvc.poll or iosvc.run?Millennium
It is safe. boost.org/doc/libs/1_56_0/doc/html/boost_asio/reference/…Polyhedron
I just wanted to accentuate that while polling the io_service or posting the yield_context into the io_service both result in cooperative single-threaded multitasking, polling the io_service avoids the context switch overhead, but unhandled exceptions from handlers will unwind and destroy the coroutine.Araucania
Calling poll_one() such way can lead to infinite stack consumption and crash.Jairia
M
0

As of Boost 1.80, Jamboree's answer no longer works. What works for me is this:

boost::asio::post(boost::asio::get_associated_executor(yield), yield);
Millennium answered 18/8, 2022 at 21:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.