How to wait for an `std::future` with boost::asio?

151 Views Asked by At

I have an asynchronous operation by an external library that I can't change, but I can give a callback to. I want to wait for it to finish but without blocking the io_context.

Something like:

// Example callback-based operation
void exampleAsyncOperation(std::function<void(boost::system::error_code)> callback) {
    std::thread t([=]() {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        callback(boost::system::error_code());
    });
    t.detach();
}

boost::asio::awaitable<void> NRClient::callAsyncOperation() {
    std::promise<boost::system::error_code> promise;
    auto future = promise.get_future();
    example_async_operation([&](boost::system::error_code ec) { promise.set_value(ec); });
    future.wait(); // this should be something that won't block the thread
}

Or is there something else I can put in the callback to get this functionality?

2

There are 2 best solutions below

0
On
4
On

You can follow the async-initiation pattern that works with CompletionToken to allow any Asio completion token, so you can await it like

co_await asyncExample(asio::deferred); // requires recent Asio, more efficient
co_await asyncExample(asio::use_awaitable); // easier to compose with awaitable operators

Here's a quick draft of such an implementation:

template <typename Token> auto asyncExample(Token&& token) {
    auto init = [=](auto handler) {
        std::thread{[h = std::move(handler)]() mutable {
            std::this_thread::sleep_for(1s);

            auto ex = asio::get_associated_executor(h);
            ex.execute([h = std::move(h)]() mutable { std::move(h)(error_code{}); });
        }}.detach();
    };
    return asio::async_initiate<Token, void(error_code)>(init, token);
}

Most - if not all - of the tricky bits have to do with the fact that you have a non-IO thread, so the invocation of the handler has to be done cautiously.

Here's a live sample Live On Coliru that

  • adds a result value so we can demonstrate the error code propagating
  • shows how to redirect/receive the error code as a tuple
#include <boost/asio.hpp>
#include <iostream>
namespace asio = boost::asio;
using boost::system::error_code;
using namespace std::chrono_literals;

template <typename Token> auto asyncExample(error_code result, Token&& token) {
    auto init = [=](auto handler) {
        std::thread{[h = std::move(handler), result]() mutable {
            std::this_thread::sleep_for(1s);

            auto ex = asio::get_associated_executor(h);
            ex.execute([h = std::move(h), result]() mutable { std::move(h)(std::move(result)); });
        }}.detach();
    };
    return asio::async_initiate<Token, void(error_code)>(init, token);
}

asio::awaitable<void> callAsyncOperation() try {
    co_await asyncExample(error_code{}, asio::deferred);
    std::cout << "First completed without error" << std::endl;

    { // redirecting the error
        error_code ec;
        co_await asyncExample(make_error_code(boost::system::errc::invalid_argument),
                redirect_error(asio::deferred, ec));
        std::cout << "Redirected error: " << ec.message() << std::endl;
    }

    { // receiving as tuple
        auto [ec] = co_await asyncExample(asio::error::eof, as_tuple(asio::deferred));
        std::cout << "Received as tuple: " << ec.message() << std::endl;
    }

    // throwing by default
    co_await asyncExample(asio::error::access_denied, asio::use_awaitable);
} catch (boost::system::system_error const& se) {
    std::cerr << "Caught: " << se.code().message() << std::endl;
}

int main() {
    asio::io_context ioc;
    co_spawn(ioc, callAsyncOperation, asio::detached);
    ioc.run();
}

Printing

First completed without error
Redirected error: Invalid argument
Received as tuple: End of file
Caught: Permission denied

UPDATE

If the entire exampleAsyncOperation function was part of a library, you can use the same technique. The only extra complication comes from passing a move-only type into the std::function argument. You need to dynamically allocate something into a copyable type for that:

Live On Coliru

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

// Example callback-based operation
namespace LIB {
    void exampleAsyncOperation(std::function<void(boost::system::error_code)> callback) {
        std::thread t([=]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            callback(boost::system::error_code());
        });
        t.detach();
    }
} // namespace LIB

template <typename Token> auto asyncExample(Token&& token) {
    return asio::async_initiate<Token, void(error_code)>(
        [=](auto handler) {
            // make copyable for std::function
            auto alloc = get_associated_allocator(handler);
            auto sh    = std::allocate_shared<decltype(handler)>(alloc, std::move(handler));

            LIB::exampleAsyncOperation([sh](error_code result) mutable {
                auto ex = asio::get_associated_executor(*sh);
                ex.execute([h = std::move(*sh), result]() mutable { std::move(h)(std::move(result)); });
            });
        },
        token);
}

asio::awaitable<void> callAsyncOperation() {
    error_code ec;
    co_await asyncExample(redirect_error(asio::deferred, ec));
    std::cout << "completed with: " << ec.message() << std::endl;
}

asio::awaitable<void> someOtherAsyncStuff() {
    auto ex = co_await asio::this_coro::executor;
    for (error_code ec; !ec;) {
        co_await asio::steady_timer(ex, 100ms).async_wait(asio::deferred);
        std::cout << "some other async work..." << std::endl;
    }
}

int main() {
    asio::io_context ioc;
    co_spawn(ioc, (callAsyncOperation() || someOtherAsyncStuff()), asio::detached);
    ioc.run();
}

For completeness I added some other async work to visibly prove that the co_await doesn't block:

some other async work...
some other async work...
some other async work...
some other async work...
some other async work...
some other async work...
some other async work...
some other async work...
some other async work...
completed with: Success