Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How is boost::asio::deferred more efficient than use_awaitable?

A recent answer by sehe mentioned that asio::deferred is more efficient than use_awaitable:

[...] Among the changes I made is replacing use_awaitable with the more efficient asio::deferred -- which is the default completion token in recent boost. [...]

I am curious about in what way it is more efficient, and if it is, in which circumstance would one use use_awaitable instead?

like image 852
Nick Matteo Avatar asked Sep 18 '25 22:09

Nick Matteo


1 Answers

asio::deferred is lightweight. It's also way more flexible than use_awaitable.

deferred

  1. As a regular completion token

    The result of an initiation function with the deferred_t token (or token adapter!) is a deferred async operation, which is basically another initiation function. It is in a way a sort of mix of std::bind and std::packaged_task for async initiations.

    E.g.

    auto op = async_write(stream, buffer/* , asio::deferred*/);
    

    Is a bit similar to std::bind in that it binds the arguments stream and buffer, and similar to std::packaged_task in that it marshals exceptions/error-codes depending on the way in which it is "consumed":

    size_t bytes_transferred = std::move(op)(asio::use_future).get(); // throws exception on error
    

    Or:

    size_t bytes_transferred = co_await std::move(op)(); // invoked with default completion token, throws on error
    bytes_transferred = co_await std::move(op);   // idem - await_transform is implemented for any async operation
    

    Or:

    auto [ec, n] = co_await std::move(op)(asio::as_tuple); // does not throw
    std::cout << "Result: " << ec.message() << std::endl;
    

    Or:

    boost::system::error_code ec;
    size_t n = co_await std::move(op)(asio::redirect_error(ec)); // does not throw
    std::cout << "Result: " << ec.message() << std::endl;
    

    etc. There are more completion tokens/adaptors: https://www.boost.org/doc/libs/1_88_0/doc/html/boost_asio/overview/model/completion_tokens.html and https://www.boost.org/doc/libs/1_88_0/doc/html/boost_asio/overview/composition/token_adapters.html

  2. As a token adaptor

    As a token adaptor, asio::deferred can facilitate async operation composition. You can see more examples of that here: https://www.boost.org/doc/libs/1_88_0/doc/html/boost_asio/examples/cpp14_examples.html#boost_asio.examples.cpp14_examples.deferred

To illustrate how deferred can become really light-weight, let's adapt an immediate value into a deferred async operation:

Live On Coliru

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

template <typename Token = asio::deferred_t> //
auto async_foo(Token&& token = {}) {
    return asio::deferred.values(42)(std::forward<Token>(token));
}

asio::awaitable<int> coro() {
    auto r = co_await async_foo(/*asio::deferred*/);
    std::cout << "Spawn: " << r << std::endl;
    co_return r;
}

int main() {
    asio::thread_pool ioc;

    std::cout << "Direct: " << async_foo(asio::use_future).get() << std::endl;
    co_spawn(ioc, coro(), asio::detached);

    ioc.join();
}

use_awaitable

use_awaitable requires c++20 coroutines.

As such it implies a coroutine promise type (basically a dynamically allocated stackframe), including a cancellation state, pending exception state, etc.

The asio promise-type associated with asio::awaitable<> return type is capable of transforming other asio::awaitable<> as well as asio::deferred_t arguments (with co_await). However, using asio::deferred_t may lead to more efficient execution when the calling context and implementation both employ coroutines.

Though use_awaitable is more expensive, it also facilitates some more high-level constructions: Coordinating Parallel Operations and Cancellation.

like image 160
sehe Avatar answered Sep 20 '25 13:09

sehe