6

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.

Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • I'd imagine that the binder returned by `bind_executor` is called from the io context and the binder then posts to the thread pool to execute your lamda. The statement in the documentation is a guarantee of the behaviour of the io context not a requirement on handlers – Alan Birtles May 31 '22 at 11:49
  • @Alan ok, I agree on the requirement part. And the io context doesn't behave as guaranteed, from what I can see, because the handler is invoked outside of `run`. – Johannes Schaub - litb May 31 '22 at 11:53
  • your handler might be but I imagine the `executor_binder` is called from the io context. The handler as far as the io context is concerned is the `executor_binder` and thats what the thread guarantee applies to – Alan Birtles May 31 '22 at 11:55
  • Binding the executor doesn't, as far as I know, create a wrapper function object. But creates a token with a `get_executor` function that tells the async operation what executor the handler is associated with (this is the task of `execution::execute`). And on that executor, the handler is then executed. Or am I mistaken? – Johannes Schaub - litb May 31 '22 at 11:56
  • I don't know how it works exactly but some quick debugging of your sample does indicate that the io context does execute the binder from its own thread then your handler is executed by the thread pool – Alan Birtles May 31 '22 at 12:03
  • @c Alan here it seems to agree with my understanding https://stackoverflow.com/a/38889147/34509 – Johannes Schaub - litb May 31 '22 at 12:45

1 Answers1

1

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++)

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Thanks for your answer! Your examle doesn't use a resource of an io_context though, when running a handler in the threadpool. In my example I used a resource that uses services of the io_context hence I think that the io_context docs are relevant (and hence using a timer). _Some_ handlers _are_ executed on ioc in my exampley as it doesn't work without ioc.run(). – Johannes Schaub - litb Jun 01 '22 at 11:44
  • 1
    However, I think maybe the manual wants to be read differently, right? This "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. " might also mean that handlers that are associated with the io_context only run within run, etc", not that all handlers executed on behalf of resources do. – Johannes Schaub - litb Jun 01 '22 at 11:47
  • Undortunately, the manual is very misleading, if that's what it means to say. I mean, the fact that something is called from within `run` doesn't even contradict the execution on `tp`. I interpreted this to mean that my handler only runs if I call `run` from within `tp`. – Johannes Schaub - litb Jun 01 '22 at 11:56
  • That would be true IFF you had bound the handler to `ioc`, not `tp`. – sehe Jun 01 '22 at 12:06
  • There's also a flip-side. You seem to have made the tacit assumption that IO objects associated with the service of a particular execution context somehow "can only run" or even "internally run their stuff" on the same threads that may execute [completion] handlers (i.e. are polling that execution context). That's not the case. Insofar the bound execution context matters, it would be about resource ownership and lifetime, not execution. The execution typically happens wherever the user initiates (async) operation, which is not constrained beyond normal thread-safety requirements. – sehe Jun 01 '22 at 12:07
  • \[_For completeness: There are siutations where indeed service implementations can use internal threads. They are by [definition implementation details and will not be observable](https://www.boost.org/doc/libs/1_79_0/doc/html/boost_asio/overview/core/threads.html#boost_asio.overview.core.threads.internal_threads) to the end user._] – sehe Jun 01 '22 at 12:11
  • TL;DR the quoted guarantees constrain where **the framework** will execute handlers (e.g. that make up composed operations or completions). It does *not* constrain where **the user** may invoke operations (including async initiation functions) or handlers (by binding another executor). Normal thread-safety requirements apply. – sehe Jun 01 '22 at 12:12
  • thanks for the clarifications. Afaik, intermediate handlers for composed operations run on the associated executor aswell, right? Just to be sure.. – Johannes Schaub - litb Jun 01 '22 at 13:30
  • Yes. And to be doubly sure: that's on the executor associated with the handler token. (This is why it's important that the handler is on a strand in case of multi-threading execution contexts, so intermediate operations on e.g. the IO object are properly synchronized). – sehe Jun 01 '22 at 13:38