c++boostboost-asio

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?


Solution

  • 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.