4

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?

Vinnie Falco
  • 5,173
  • 28
  • 43
he rambled
  • 154
  • 2
  • 8
  • 1
    I would like to know the answer to this as well. And your question exactly reflects my thoughts on the matter. The semantics of `asio_handler_is_continuation` are different from the use of `post` versus `defer`. – Vinnie Falco May 04 '18 at 15:25
  • @VinnieFalco We have to use `asio_handler_is_continuation` in this case. See my answer. – he rambled May 05 '18 at 11:04

3 Answers3

5

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'

update:

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

Community
  • 1
  • 1
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • 1
    But I cant call `defer(executor, handler)` because `async_read_until` is responsible to call the handler. Can you provide an example? – he rambled May 04 '18 at 15:04
  • 1
    @herambled I can't. There's doesn't seem to be enough information in the asio docs. – Richard Hodges May 04 '18 at 15:32
  • 1
    That minimalistic documentation... Anyway, thank you for the effort to write an answer. I think I found a *kind of* workaround. – he rambled May 04 '18 at 15:38
  • 1
    @herambled In the past I have emailed Christopher Kohlhoff for help. He has graciously responded twice without mentioning money. He might be happy to explain it - particularly if you offer to update the documentation for him ;) – Richard Hodges May 04 '18 at 15:40
  • 1
    @herambled see update. You manually extract the handler's function and pass it into your own handler. – Richard Hodges May 04 '18 at 15:59
  • 1
    This is my goto de-facto documentation: https://www.boost.org/doc/libs/develop/libs/beast/doc/html/beast/using_io/writing_composed_operations.html. – sehe May 04 '18 at 16:15
  • 1
    PS I think it's a bit hostile to _downvote_ an excellent answer from someone with heaps of real life Asio expertise. If it's not helpful/doesn't solve your problem, don't upvote, and discuss. We're all here sharing. – sehe May 04 '18 at 16:17
  • 1
    I see I could use the technique in your update for my 'workaround'. My workaround is to provide a custom executor that have `post` function that conditionaly redirects to `defer`. I'll post my workaround after testing it. – he rambled May 04 '18 at 16:38
  • 2
    @herambled That is the approach described here: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4242.html#executors.on_the_need_for_dispatch__post_and_defer – Vinnie Falco May 07 '18 at 14:32
  • 1
    @VinnieFalco Oh, it's from author of Boost.asio. I actually like that idea, it is more intuitive way. It's pity that current version of `async_read` does not use `_ex.post` to schedule the handler. Well, they only added `defer` at Boost 1.66, which is fairly recent version, and in the future more modern features will be introduced. So the behavior described in the paper might get into the Boost.asio and, hopefully Networking.TS! – he rambled May 07 '18 at 15:22
  • 1
    First of all, there's no "they" there's just "him" (lol). This is what he had to say: "N.B: Most async operations implementations within asio never "complete immediately" according to the networking TS definition, meaning they always use ex.dispatch() and never ex.post(). This is a todo item." I guess that agrees with your observation. – Vinnie Falco May 07 '18 at 15:26
  • @VinnieFalco Great! I eager to see "him" implements that :). And I hope Networking.TS explicitly mention about it, instead of leaving it to the complier makers. – he rambled May 07 '18 at 15:46
2

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.

he rambled
  • 154
  • 2
  • 8
  • 1
    This sounds good in theory, but Networking.TS has no equivalent to `asio_handler_is_continuation`. Is that functionality going away? – Vinnie Falco May 05 '18 at 12:30
  • @VinnieFalco I think so. Without controlling the way how internal operation is submitted to `io_context`, The effect we want from `asio_handler_is_continuation` is impossible. And upon a quick looking, Networking.TS does not provide a concrete restriction on how work is submitted to `io_context`. So, unless they provide an indicator like `net_handler_is_continuation`, that functionality is certainly going away :/ – he rambled May 05 '18 at 14:07
2

It seems, that the comments in the code found on https://github.com/chriskohlhoff/asio-tr2/blob/master/doc/executors.qbk actually contain the most detailed description about dispatch(), post() and defer().

defer() postpones the processing of the newly created job to after the current job has finished. It does not postpone it past other queued jobs. postponing to after the completion of the current job has the big advantage, that the new job can then run in the same thread as the current job. The default executor will try to do so. And as the new job will most likely use some or more data from the current job, staying in the same thread and thus the same CPU core greatly improves cache locality and thus reduces total execution time and increases throughput.

In other words: In those of your completion handlers / tasks, that launch exactly one new completion handler / task, you almost always want to use defer() instead of post(). That's especially true, if the new task is launched towards the end of the current task.

However, those tasks, that launch more than one new task, should only submit the most related task (which will often be the last task) via defer(), and use post() for all the other ones.

Only for those tasks, that are really simple, think about submitting them via dispatch() instead of post() or queue(): If rules permit it (like the strand, that they are dispatched into, has currently an empty queue), then they will run directly, avoiding all the queuing and unqueuing latency.

Kai Petzke
  • 2,150
  • 21
  • 29
  • Note that the text reflects the 2015 [P0113R0](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0113r0.html) which revises N4242. – sehe May 26 '22 at 21:12