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 efficientasio::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?
asio::deferred
is lightweight. It's also way more flexible than use_awaitable
.
deferred
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
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:
#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.