Home > OS >  ASIO: Is it defined behavior to run a completion handler in a different thread?
ASIO: Is it defined behavior to run a completion handler in a different thread?

Time:06-02

In the following example, the timer was associated with the io_context executor. But then, the handler is told to execute in the thread-pool. The reason is, because the handler actually executes blocking code, and I don't want to block the run function of the io_context.

But the documentation states

Handlers are invoked only by a thread that is currently calling any overload of run(), run_one(), run_for(), run_until(), poll() or poll_one() for the io_context.

As the code shows, the handler is invoked by the thread_pool, outside of run. Is this behavior well-defined? Does it work also for sockets?

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

int main() {
    using namespace boost::asio;
    thread_pool tp(4);

    io_context ioc;
    deadline_timer dt(ioc, boost::posix_time::milliseconds(500));
    dt.async_wait(bind_executor(tp, [&](boost::system::error_code){
        std::cout << "running in tp: " << tp.get_executor().running_in_this_thread() << std::endl;
        std::cout << "running in ioc: " << ioc.get_executor().running_in_this_thread() << std::endl;
        exit(0);
    }));

    auto wg = make_work_guard(ioc);
    ioc.run(); 
}
running in tp: 1
running in ioc: 0

Godbolt link.

CodePudding user response:

First things first.

You can run a handler anywhere you want. Whether it results in UB depends on what you do in the handler.

I'll interpret your question as asking "Does the observed behaviour contradict the documented requirements/guarantees for handler invocation?"

Does This Contradict Documentation?

It does not.

You have two different execution contexts. One of them is io_context (which you linked to the documentation of), and the other is thread_pool.

You've asked to invoke the handler on tp (by binding the handler to its executor).

Therefore, the default executor that deadline_timer was bound to on construction is overruled by the handler's associated executor.

You're confusing yourself by looking at the documentation for io_context that doesn't govern this case: The completion to the deadline-timer is never posted to the io_context. It's posted to the thread_pool's context, as per your specific request.

As expected, the handler is invoked on a thread running the execution context tp.

Simplify And Elaborate

Here's a simplified example that uses post directly, instead of going through an arbitrary async initiation function to do that.

In addition it exercises all the combinations of target executor and associated executors, if any.

Live On Coliru

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

int main() {
    std::cout << std::boolalpha << std::left;

    asio::io_context ioc;
    asio::thread_pool tp(4);

    auto xioc = ioc.get_executor();
    auto xtp  = tp.get_executor();

    static std::mutex mx; // prevent jumbled output

    auto report = [=](char const* id, auto expected) {
        std::lock_guard lk(mx);

        std::cout << std::setw(11) << id << " running in tp/ioc? "
                  << xtp.running_in_this_thread() << '/'
                  << xioc.running_in_this_thread() << " "
                  << (expected.running_in_this_thread() ? "Ok" : "INCORRECT")
                  << std::endl;
    };

    asio::post(tp,  [=] { report("direct tp", xtp); });
    asio::post(ioc, [=] { report("direct ioc", xioc); });

    asio::post(ioc, bind_executor (tp,  [=] { report("bound tp.A", xtp); }));
    asio::post(tp,  bind_executor (tp,  [=] { report("bound tp.B", xtp); }));
    asio::post(     bind_executor (tp,  [=] { report("bound tp.C", xtp); }));

    asio::post(ioc, bind_executor (ioc, [=] { report("bound ioc.A", xioc); }));
    asio::post(tp,  bind_executor (ioc, [=] { report("bound ioc.B", xioc); }));
    asio::post(     bind_executor (ioc, [=] { report("bound ioc.C", xioc); }));

    // system_executor doesn't have .running_in_this_thread()
    // asio::post([=] { report("system", asio::system_executor{}); });

    ioc.run();
    tp.join();
}

Prints e.g.

direct tp   running in tp/ioc? true/false Ok
direct ioc  running in tp/ioc? false/true Ok
bound tp.C  running in tp/ioc? true/false Ok
bound tp.B  running in tp/ioc? true/false Ok
bound tp.A  running in tp/ioc? true/false Ok
bound ioc.C running in tp/ioc? false/true Ok
bound ioc.B running in tp/ioc? false/true Ok
bound ioc.A running in tp/ioc? false/true Ok

Note that

  • the bound executor always prevails
  • there's a fallback to system executor (see its effect)

For semantics of post see the docs or e.g. When must you pass io_context to boost::asio::spawn? (C )

  • Related