On Boost 1.66, Asio have deprecated the asio_handler_is_continuation
hook function, promoting usage of defer
function. It seems that defer
function is behaving exactly the same as post
when asio_handler_is_continuation==true. However, the way of using defer
is different from the way of using asio_handler_is_continuation
, and I am not sure how to properly use defer
.
EDIT: I think the sample below is too verbose to clearly express what i mean. Here's shorter example:
async_read_until(stream, read_buffer, "\r\n",
[](boost::system::error_code ec, std::size_t bytes_transferred)
{
if(!ec)
async_write(stream, write_buffer, some_handler);
})
Now when async_read_until
is completed, the lambda handler passed will be invoked using some means equivalent to boost::asio::post
. But async_write
inside the lambda handler is continuation from last async task, So I want to invoke the lambda handler using defer
to take adventage of optimization.
Is there any way to use defer
(instead of post
) to invoke the lambda handler in above example?
ORIGINAL POST: I am trying to write a simple initating function async_echo
similar to the one in beast document, except that the part that calls boost::asio::async_write
will be called as a continuation. To achieve this, prior intermediate operation boost::asio::async_read_until
must call the handler *this
as a continuation.
This is the part that I am referring in the async_echo example of the beast document:
template<class AsyncStream, class Handler>
void echo_op<AsyncStream, Handler>::
operator()(boost::beast::error_code ec, std::size_t bytes_transferred)
{
// Store a reference to our state. The address of the state won't
// change, and this solves the problem where dereferencing the
// data member is undefined after a move.
auto& p = *p_;
// Now perform the next step in the state machine
switch(ec ? 2 : p.step)
{
// initial entry
case 0:
// read up to the first newline
p.step = 1;
return boost::asio::async_read_until(p.stream, p.buffer, "\r", std::move(*this));
case 1:
// write everything back
p.step = 2;
// async_read_until could have read past the newline,
// use buffers_prefix to make sure we only send one line
return boost::asio::async_write(p.stream,
boost::beast::buffers_prefix(bytes_transferred, p.buffer.data()), std::move(*this));
case 2:
p.buffer.consume(bytes_transferred);
break;
}
// Invoke the final handler. The implementation of `handler_ptr`
// will deallocate the storage for the state before the handler
// is invoked. This is necessary to provide the
// destroy-before-invocation guarantee on handler memory
// customizations.
//
// If we wanted to pass any arguments to the handler which come
// from the `state`, they would have to be moved to the stack
// first or else undefined behavior results.
//
p_.invoke(ec);
return;
}
On pre-1.66 days, I could simply hook the function as follows:
template <Function, Handler>
friend bool asio_handler_is_continuation(echo_op<Function, Handler>* handler)
{
using boost::asio::asio_handler_is_continuation;
return handler.p_->step == 1 ||
asio_handler_is_continuation(std::addressof(handler.p_->handler()));
}
inside the declaration of echo_op
.
Starting from Boost 1.66, the code above is not likely to have any effect (without BOOST_ASIO_NO_DEPRECATION
macro). So I should be using defer
.
But since boost::asio::async_read_until
has a guarantee that "Invocation of the handler will be performed in a manner equivalent to using boost::asio::io_context::post().", *this
will not be invoked using defer
, that is, as a continuation.
Is there any workaround that makes boost::asio::async_read_until
invoke the handler using defer
? And is there any good examples that utilize defer
function?
After playing around a bit, it turns out that asio_handler_is_continuation
is not deprecated; and there is no way to replace it with defer
currently.
To redirect any post
calls to defer
, I provided following custom executor:
template<typename UnderlyingExecutor, typename std::enable_if<boost::asio::is_executor<UnderlyingExecutor>::value, int>::type = 0>
class continuation_executor
{
private:
UnderlyingExecutor _ex;
public:
continuation_executor(UnderlyingExecutor ex)
:_ex(ex){}
template<class Function, class Allocator>
void post(Function f, Allocator a)
{
std::cout<<"Redirected to defer()"<<std::endl;
_ex.defer(BOOST_ASIO_MOVE_CAST(Function)(f),a);
}
template<class Function, class Allocator>
void defer(Function f, Allocator a)
{
std::cout<<"defer() called"<<std::endl;
_ex.defer(BOOST_ASIO_MOVE_CAST(Function)(f),a);
}
template<class Function, class Allocator>
void dispatch(Function f, Allocator a)
{
std::cout<<"dispatch() called"<<std::endl;
_ex.dispatch(BOOST_ASIO_MOVE_CAST(Function)(f),a);
}
auto context() -> decltype(_ex.context())
{
return _ex.context();
}
void on_work_started()
{
_ex.on_work_started();
}
void on_work_finished()
{
_ex.on_work_finished();
}
};
It is really a trivial executor, relying entirely to the underlying executor, with continuation_executor::post
that redirects into underlying executor's defer
.
But when I pass a handler to async_read_some
with something like bind_executor(conti_exec, handler)
, I get following output:
dispatch() called
So the passed handler does not get scheduled via post()
; it is scheduled by some other means. Concretely, built-in async function like asio::async_read_some
schedule the internal operation object via scheduler::post_immediate_completion
, then io_context::run
executes the operation.
Upon completion of the async operation, complete
method of the operation object is called to execute the user-provided handler. That complete
method, at least on current implementation, uses associated executor's dispatch
method to run the handler. There is no place for above hook. So it is completely obsolate; attempt to use defer
instead of asio_handler_is_continuation
is out of luck.
What I stated on my question, "Starting from Boost 1.66, the code above is not likely to have any effect (without BOOST_ASIO_NO_DEPRECATION macro).", is plain wrong. asio_handler_is_continuation
is still in effect, and it is not deprecated as of 1.67.
This is the evidence that asio_handler_is_continuation
is still in effect:
// Start an asynchronous send. The data being sent must be valid for the
// lifetime of the asynchronous operation.
template <typename ConstBufferSequence, typename Handler>
void async_send(base_implementation_type& impl,
const ConstBufferSequence& buffers,
socket_base::message_flags flags, Handler& handler)
{
bool is_continuation =
boost_asio_handler_cont_helpers::is_continuation(handler);
// Allocate and construct an operation to wrap the handler.
typedef reactive_socket_send_op<ConstBufferSequence, Handler> op;
typename op::ptr p = { boost::asio::detail::addressof(handler),
op::ptr::allocate(handler), 0 };
p.p = new (p.v) op(impl.socket_, impl.state_, buffers, flags, handler);
BOOST_ASIO_HANDLER_CREATION((reactor_.context(), *p.p, "socket",
&impl, impl.socket_, "async_send"));
start_op(impl, reactor::write_op, p.p, is_continuation, true,
((impl.state_ & socket_ops::stream_oriented)
&& buffer_sequence_adapter<boost::asio::const_buffer,
ConstBufferSequence>::all_empty(buffers)));
p.v = p.p = 0;
}
Note that it uses boost_asio_handler_cont_helpers
to find out if the handler is continuation. boost_asio_handler_cont_helpers
internally invokes asio_handler_is_continuation
.
async_send
is used by async_write_some
internally. I did not check every built-in async tasks that asio library provides, But I am pretty sure that other async tasks executes it's handler the same way.
So, if you want built-in async tasks to execute your handler as continuation, you will have to rely on asio_handler_is_continuation
. defer
is not going to entirely replace it! defer
can only be used when you schedule your handler directly from your code.
This has puzzled me in the past also.
Executor::defer
and Executor::post
both perform the same operation, except for this note:
Note: Although the requirements placed on defer are identical to post, the use of post conveys a preference that the caller does not block the first step of f1's progress, whereas defer conveys a preference that the caller does block the first step of f1. One use of defer is to convey the intention of the caller that f1 is a continuation of the current call context. The executor may use this information to optimize or otherwise adjust the way in which f1 is invoked. —end note
https://www.boost.org/doc/libs/1_67_0/doc/html/boost_asio/reference/Executor1.html
So it seems that responsibility to chaining continuations has been made an implementation detail of the Executor
model.
Which as far as I can tell, means that all you need to do is call the free function defer(executor, handler)
and the executor will 'do the right thing'
Found some documentation which shows how to chain handlers through the final executor:
source of documentation: https://github.com/chriskohlhoff/asio-tr2/blob/master/doc/executors.qbk
example: https://github.com/chriskohlhoff/executors/blob/v0.2-branch/src/examples/executor/async_op_2.cpp
see lines 38+ in async_op_2.cpp
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With