How can I get a future from boost::asio::post?
Asked Answered
P

3

9

I am using Boost 1.66.0, in which asio has built-in support for interoperating with futures (and for some time now). The examples I've seen online indicate how to achieve this cleanly when using networking functions such as async_read, async_read_some, etc. That is done by providing boost::asio::use_future in place of the completion handler, which causes the initiating function to return a future as expected.

What kind of object do I need to provide or wrap my function in to get the same behavior from boost::asio::post?

My purpose for posting the work is to execute it in the context of a strand but otherwise wait for the work to complete, so I can get the behavior I want doing:

std::packaged_task<void()>  task( [] { std::cout << "Hello world\n"; } );
auto  f = task.get_future();
boost::asio::post(
    boost::asio::bind_executor(
        strand_, std::move( task ) ) );
f.wait();

but according to the boost::asio documentation, the return type for boost::asio::post is deduced in the same way as for functions like boost::asio::async_read, so I feel like there has to be a nicer way that can avoid the intermediate packaged_task. Unlike async_read there is no "other work" to be done by post so providing just boost::asio::use_future doesn't makes sense, but we could define an async_result trait to get the same behavior for post.

Is there a wrapper or something that has the necessary traits defined to get the behavior I want or do I need to define it myself?

Prate answered 20/4, 2018 at 22:55 Comment(1)
I have the exactly same question, it seems the only way is define serveral specfic type for the function like async_result<use_future_t<Allocator>, Result(Args...)> do.Your packaged_task solution is much cleanerTunnage
H
6

@MartiNitro's idea with packaged_task has become part of the library: now you can just post a packaged_task and it will magically return its future:

    auto f = post(strand_, std::packaged_task<int()>(task));

Update

Thanks to @niXman's comment, discovered a more convenient interface on the use_future token:

    auto f = post(ex, boost::asio::use_future(task));

Live Demo

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

int task() {
    std::this_thread::sleep_for(1s);
    std::cout << "Hello world\n";
    return 42;
}

int main() {
    boost::asio::thread_pool ioc(1);
    auto                     ex = ioc.get_executor();

    auto f = post(ex, std::packaged_task<int()>(task));
    // optionally wait for future:
    f.wait();
    // otherwise .get() would block:
    std::cout << "Answer: " << f.get() << "\n";

    f = post(ex, boost::asio::use_future(task));
    f.wait();
    std::cout << "Second answer: " << f.get() << "\n";

    ioc.join();
}

Prints

Hello world
Answer: 42
Hello world
Second answer: 42
Hitherto answered 17/11, 2021 at 16:9 Comment(3)
is there any reasons not to use use_future(F), ie post(strand, use_future([](){ return 42;})) ?Tanney
@Tanney None, except that I didn't know about it. As far as I can tell all of this flew under the radar and I happened to discover the equivalent route. Thanks for adding - it does look as if it has been there ever since 1.66?. And I'm somewhat religiously spelling out docs and change history...Hitherto
yeah, it looks like that =) It's okay, when using asio it's normal, in 10 years the documentation will be fixed.Tanney
H
8

UPDATE: With more recent boost, use this much simpler answer


What kind of object do I need to provide or wrap my function in to get the same behavior from boost::asio::post?

You can't. post is a void operation. So the only option to achieve it with post is to use a packaged-task, really.

The Real Question

It was hidden in the part "how to get the same behaviour" (just not from post):

template <typename Token>
auto async_meaning_of_life(bool success, Token&& token)
{
    using result_type = typename asio::async_result<std::decay_t<Token>, void(error_code, int)>;
    typename result_type::completion_handler_type handler(std::forward<Token>(token));

    result_type result(handler);

    if (success)
        handler(error_code{}, 42);
    else
        handler(asio::error::operation_aborted, 0);

    return result.get ();
}

You can use it with a future:

std::future<int> f = async_meaning_of_life(true, asio::use_future);
std::cout << f.get() << "\n";

Or you can just use a handler:

async_meaning_of_life(true, [](error_code ec, int i) {
    std::cout << i << " (" << ec.message() << ")\n";
});

Simple demo: Live On Coliru

Extended Demo

The same mechanism extends to supporting coroutines (with or without exceptions). There's a slightly different dance with async_result for Asio pre-boost 1.66.0.

See all the different forms together here:

Live On Coliru

#define BOOST_COROUTINES_NO_DEPRECATION_WARNING 
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/use_future.hpp>

using boost::system::error_code;
namespace asio = boost::asio;

template <typename Token>
auto async_meaning_of_life(bool success, Token&& token)
{
#if BOOST_VERSION >= 106600
    using result_type = typename asio::async_result<std::decay_t<Token>, void(error_code, int)>;
    typename result_type::completion_handler_type handler(std::forward<Token>(token));

    result_type result(handler);
#else
    typename asio::handler_type<Token, void(error_code, int)>::type
                 handler(std::forward<Token>(token));

    asio::async_result<decltype (handler)> result (handler);
#endif

    if (success)
        handler(error_code{}, 42);
    else
        handler(asio::error::operation_aborted, 0);

    return result.get ();
}

void using_yield_ec(asio::yield_context yield) {
    for (bool success : { true, false }) {
        boost::system::error_code ec;
        auto answer = async_meaning_of_life(success, yield[ec]);
        std::cout << __FUNCTION__ << ": Result: " << ec.message() << "\n";
        std::cout << __FUNCTION__ << ": Answer: " << answer << "\n";
    }
}

void using_yield_catch(asio::yield_context yield) {
    for (bool success : { true, false }) 
    try {
        auto answer = async_meaning_of_life(success, yield);
        std::cout << __FUNCTION__ << ": Answer: " << answer << "\n";
    } catch(boost::system::system_error const& e) {
        std::cout << __FUNCTION__ << ": Caught: " << e.code().message() << "\n";
    }
}

void using_future() {
    for (bool success : { true, false }) 
    try {
        auto answer = async_meaning_of_life(success, asio::use_future);
        std::cout << __FUNCTION__ << ": Answer: " << answer.get() << "\n";
    } catch(boost::system::system_error const& e) {
        std::cout << __FUNCTION__ << ": Caught: " << e.code().message() << "\n";
    }
}

void using_handler() {
    for (bool success : { true, false })
        async_meaning_of_life(success, [](error_code ec, int answer) {
            std::cout << "using_handler: Result: " << ec.message() << "\n";
            std::cout << "using_handler: Answer: " << answer << "\n";
        });
}

int main() {
    asio::io_service svc;

    spawn(svc, using_yield_ec);
    spawn(svc, using_yield_catch);
    std::thread work([] {
            using_future();
            using_handler();
        });

    svc.run();
    work.join();
}

Prints

using_yield_ec: Result: Success
using_yield_ec: Answer: 42
using_yield_ec: Result: Operation canceled
using_yield_ec: Answer: 0
using_yield_catch: Answer: 42
using_future: Answer: 42
using_yield_catch: Caught: Operation canceled
using_future: Answer: using_future: Caught: Operation canceled
using_handler: Result: Success
using_handler: Answer: 42
using_handler: Result: Operation canceled
using_handler: Answer: 0
Hitherto answered 21/4, 2018 at 12:43 Comment(4)
Thanks. This isn't what I was looking for but it was what I needed. Essentially, there's nothing special about post, and by using the types and facilities that asio has we can make an interface as generic (supporting multiple ways of calling as you've demonstrated) or narrow as we like.Prate
clear simple explanation.I just have some questions.what is relation between signature and completion_handler_type???also i always considered that async operation is magical thing which is different than normal function but i can not define where magic happens...your answer shows the formation of async operation"like async_read...." but i do not see any difference from normal operation.does async_result part is the part responsible for making this function working asynchronously????Thirst
what is see is that :you make function which takes bool and universal reference to "something" then you make alias to specialization of template then you make instance of completion_handler_type_ of this specialized template then you make instance of the specialized template.....till now what makes this function work asynchronously?????....then you make some logic then you returned the result of method get of specialized template???....I am walking step by step to be able to put my finger on the step which make this function works asynchronously...thanks in advanceThirst
I stumbled upon a reddit article from the boost::beast author here: reddit.com/r/cpp/comments/6aygos/… That has a wealth of information for boost::beast developers. Which itself refers to the author's tutorial here: boost.org/doc/libs/1_69_0/libs/beast/doc/html/beast/using_io/… I found these resources immensly helpful.Roti
H
6

@MartiNitro's idea with packaged_task has become part of the library: now you can just post a packaged_task and it will magically return its future:

    auto f = post(strand_, std::packaged_task<int()>(task));

Update

Thanks to @niXman's comment, discovered a more convenient interface on the use_future token:

    auto f = post(ex, boost::asio::use_future(task));

Live Demo

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

int task() {
    std::this_thread::sleep_for(1s);
    std::cout << "Hello world\n";
    return 42;
}

int main() {
    boost::asio::thread_pool ioc(1);
    auto                     ex = ioc.get_executor();

    auto f = post(ex, std::packaged_task<int()>(task));
    // optionally wait for future:
    f.wait();
    // otherwise .get() would block:
    std::cout << "Answer: " << f.get() << "\n";

    f = post(ex, boost::asio::use_future(task));
    f.wait();
    std::cout << "Second answer: " << f.get() << "\n";

    ioc.join();
}

Prints

Hello world
Answer: 42
Hello world
Second answer: 42
Hitherto answered 17/11, 2021 at 16:9 Comment(3)
is there any reasons not to use use_future(F), ie post(strand, use_future([](){ return 42;})) ?Tanney
@Tanney None, except that I didn't know about it. As far as I can tell all of this flew under the radar and I happened to discover the equivalent route. Thanks for adding - it does look as if it has been there ever since 1.66?. And I'm somewhat religiously spelling out docs and change history...Hitherto
yeah, it looks like that =) It's okay, when using asio it's normal, in 10 years the documentation will be fixed.Tanney
F
2

Thas what I came up with, it essentially wrapts the asio::post and plugs in a promise/future pair. I think it can be adapted to your needs as well.

// outer scope setup
asio::io_context context;
asio::io_context::strand strand(context);


std::future<void> async_send(tcp::socket& socket, std::string message) {
    auto buffered = std::make_shared<std::string>(message);
    std::promise<void> promise;
    auto future = promise.get_future();

    // completion handler which only sets the promise.
    auto handler = [buffered, promise{std::move(promise)}](asio::error_code, std::size_t) mutable {
        promise.set_value();
    };

    // post async_write call to strand. Thas *should* protecte agains concurrent 
    // writes to the same socket from multiple threads
    asio::post(strand, [buffered, &socket, handler{std::move(handler)}]() mutable {
        asio::async_write(socket, asio::buffer(*buffered), asio::bind_executor(strand, std::move(handler)));
    });
    return future;
}

The promise can be moved without the future becoming invalidated.

Adapted to your scenario it could be somethign like this:

template<typename C>
std::future<void> post_with_future(C&& handler)
{
    std::promise<void> promise;
    auto future = promise.get_future();

    auto wrapper = [promise{std::move(promise)}]{ // maybe mutable required?
         handler();
         promise.set_value();
    };

    // need to move in, cause the promise needs to be transferred. (i think)
    asio::post(strand, std::move(wrapper)); 
    return future;
}

I would be happy about some feedback to those lines, as I am myself just learning the whole thing :)

Hope to help, Marti

Fleshly answered 25/6, 2020 at 16:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.