0

I'm trying to implement my own boost::asio operation, executed asynchronously during io_context.run().

Also I need to implement a cancellable operation which waits for some condition / predicate is met (or boost::signals2::signal is called) - and next, calls any completion handler in io_context thread? Is such functionality already available in Boost?

My code is inspired by Boost documentation:

#include <boost/asio.hpp>
#include <iostream>

/// From boost documentation:
/// — If the initiating function is not a member function,
/// the associated executor is that returned by the get_executor member function
/// of the first argument to the initiating function.
struct executor_owner
{
    using executor_type = boost::asio::io_context;
    executor_type* m_executor = nullptr;

    explicit executor_owner(executor_type& ex)
        : m_executor(&ex)
    {
    }

    executor_type& get_executor() BOOST_ASIO_NOEXCEPT
    {
        std::cout << "get_executor() called." << std::endl;
        return *m_executor;
    }
};

/// My initiating function
template<class CompletionToken>
auto async_xyz(executor_owner executorOwner, CompletionToken&& token)
{
    using completion_handler_t = typename boost::asio::async_completion<CompletionToken, void(executor_owner&)>::completion_handler_type;
    return boost::asio::async_initiate<CompletionToken, void(executor_owner&)>(
        [](completion_handler_t completion_handler, executor_owner& owner) {
            // initiate the operation and cause completion_handler to be invoked
            // with the result
            std::cout << "I'm performing the async func operation here!" << std::endl;
            completion_handler();
        },
        token, executorOwner);
}

int main()
{
    boost::asio::io_context io_context;
    executor_owner executorOwner(io_context);

    async_xyz(executorOwner, []()
    {
        std::cout << "completion handler here!" << std::endl;
    });

    // I expect that completion handler will be called asynchronously, during io_context::run().
    // Unfortunately, completion handler has already been called.

    // std::cout << "io_context begin." << std::endl;
    // io_context.run(); // It doesn't matter. completion handler
    // std::cout << "io_context done." << std::endl;

    return 0;
}


// Output:
// I'm performing the async func operation here!
// completion handler here!

My questions:

  1. How to implement custom async operation compliant with boost::asio design?
  2. Why my executor wasn't obtained using get_executor() method as described in boost documentation?
  3. Does boost::asio::async_initiate internally posts operation to io_context, or have I call io_context::post(...) manually in my custom operation implementation (to trigger the completion handler)?
  4. How to implement easy wait operation which waits for a met predicate or a signal?
  5. How to add timeout / cancellation functionality to such operation?

I'm using boost 1.82 and C++14 (but C++11 solution is preferred).

All hints welcome, regards. And in case of down vote, please explain what's wrong.

Michał Jaroń
  • 469
  • 4
  • 11
  • You're asking too many questions at once. In effect, you're asking for the whole async completion mechanism to be explained to you in a SO answer. That won't work. I've addressed the issues with your code in my answer. Look at [per-operation cancellation](https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio/overview/core/cancellation.html) – sehe Jul 15 '23 at 20:52
  • A met predicate or signal: I usually set an "infinite" timer which can be cancelled to signal an event. – sehe Jul 15 '23 at 20:53
  • By the way, welcome to Asio. It's an awesome framework - though *building* asynchronous operations has a pretty steep learning curve, so just a fair warning you'll probably be here more often. Which is fine, as long as you keep your questions more focused :) – sehe Jul 15 '23 at 20:54

1 Answers1

0

A few notes

/// From boost documentation:
/// — If the initiating function is not a member function,
/// the associated executor is that returned by the get_executor member function
/// of the first argument to the initiating function.

The first argument to the initiating function is completion_handler_t. If you need to bind an executor to it, simply use bind_executor. (The other common method is implementing a hand-coded initiation function object that has the associated executor).

    // initiate the operation and cause completion_handler to be invoked
    // with the result

You should dispatch/post it to "cause [it] to be invoked":

asio::dispatch(completion_handler);

Thus, everything simplifies to:

Live On Coliru

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

/// My initiating function
template <typename CompletionToken> auto async_xyz(CompletionToken&& token) {
    return asio::async_initiate<CompletionToken, void()>(
        [](auto completion_handler) {
            // initiate the operation and cause completion_handler to be invoked
            // with the result
            std::cout << "I'm performing the async func initiation here!" << std::endl;
            asio::dispatch(completion_handler);
        },
        token);
}

int main() {
    asio::io_context io_context;

    auto f = [] { std::cout << "completion handler here!" << std::endl; };
    async_xyz(bind_executor(io_context.get_executor(), f));

    std::cout << "io_context begin." << std::endl;
    io_context.run();
    std::cout << "io_context done." << std::endl;
}

Printing the expected

I'm performing the async func initiation here!
io_context begin.
completion handler here!
io_context done.
sehe
  • 374,641
  • 47
  • 450
  • 633
  • PS. the executor-less `post`/`dispatch` actively assumes the handler has had an executor associated. If not, it will fallback to a `system_executor` (see e.g. [here](https://stackoverflow.com/questions/52458609/which-io-context-does-stdboostasiopost-dispatch-use/52460852#52460852)) – sehe Jul 15 '23 at 21:11