Is calling asio io_service poll() or poll_one() in a nested or recursive fashion (ie. within a handler) valid?

boost-asio, c++

Solution

It is valid.

For the family of functions that process the `io_service`, `run()` is the only one with restrictions:

The `run()` function must not be called from a thread that is currently calling one of `run()`, `run_one()`, `poll()` or `poll_one()` on the same `io_service` object.

However, I am inclined to think that the documentation should also include the same remark for `run_one()`, as a nested call can result in it blocking indefinitely for either of the following cases[1]:

- the only work in the `io_service` is the handler currently being executed

- for non I/O completion port implementations, the only work was posted from within the current handler and the `io_service` has a concurrency hint of `1`

For Windows I/O completion ports, demultiplexing is performed in all threads servicing the `io_service` using `GetQueuedCompletionStatus()`. At a high-level, threads calling `GetQueuedCompletionStatus()` function as if they are part of a thread pool, allowing the OS to dispatch work to each thread. As no single thread is responsible for demultiplexing operations to other threads, nested calls to `poll()` or `poll_one()` do not affect operation dispatching for other threads. The documentation states:

Demultiplexing using I/O completion ports is performed in all threads that call `io_service::run()`, `io_service::run_one()`, `io_service::poll()` or `io_service::poll_one()`.

For all other demultiplexing mechanisms systems, a single thread servicing `io_service` is used to demultiplex I/O operations. The exact demultiplexing mechanism can be found in the Platform-Specific Implementation Notes:

Demultiplexing using [`/dev/poll`, `epoll`, `kqueue`, `select`] is performed in one of the threads that calls `io_service::run()`, `io_service::run_one()`, `io_service::poll()` or `io_service::poll_one()`.

The implementation for the demultiplexing mechanism differs slightly, but at a high-level:

- the `io_service` has a main queue from which threads consume ready-to-run operations to perform

- each call to process the `io_service` creates a private queue on the stack that is used to manage operations in a lock-free manner

- synchronization with the main queue eventually occurs, where a lock is acquired and the private queue operations are copied into the main queue, informing other threads, and allowing them to consume from the main queue.

When the `io_service` is constructed, it may be provided a concurrency hint, suggesting how many threads the implementation should allow to run concurrently. When non-I/O completion port implementations are provided a concurrency hint of `1`, they are optimized to use the private queue as much as possible and defer synchronization with the main queue. For example, when a handler is posted via `post()`:

- if invoked from outside of a handler, then the `io_service` guarantees thread safety so it locks the main queue before enqueueing the handler.

- if invoked from within a handler, the posted handler is enqueued into the private queue, deferring deferring synchronization with the main queue until necessary.

When a nested `poll()` or `poll_one()` is invoked, it becomes necessary for the private queue to be copied into the main queue, as operations to be performed will be consumed from the main queue. This case is explicitly checked within the implementation:

// We want to support nested calls to poll() and poll_one(), so any handlers
// that are already on a thread-private queue need to be put on to the main
// queue now.
if (one_thread_)
  if (thread_info* outer_thread_info = ctx.next_by_key())
    op_queue_.push(outer_thread_info->private_op_queue);

When either no concurrency hint or any value other than `1` is provided, then posted handlers are synchronized into the main queue each time. As the private queue does not need to be copied, nested `poll()` and `poll_one()` calls will function as normal.

1. In the networking-ts draft, it is noted that `run_one()` must not be called from a thread that is currently calling `run()`.

Problem

Is calling asio::io_service::poll() or poll_one() in a nested or recursive fashion (ie. from within a handler) valid? A really basic test seems to imply that this works (I've only done the test on one platform) but I want to be sure that calling poll() again from within a handler is considered valid behavior. I couldn't find any relevant information in the asio docs, so I'm hoping that someone with a bit more experience with asio's inner workings could verify this with an explanation or references. Basic test: ``` struct NestedHandler { NestedHandler(std::string name, asio::io_service * service) : name(name), service(service) { // empty } void operator()() { std::cout << " { "; std::cout << name; std::cout << " ...calling poll again... "; service->poll(); std::cout << " } "; } std::string name; asio::io_service * service; }; struct DefaultHandler { DefaultHandler(std::string name) : name(name) { // empty } void operator()() { std::cout << " { "; std::cout << name; std::cout << " } "; } std::string name; }; int main() { asio::io_service service; service.post(NestedHandler("N",&service)); service.post(DefaultHandler("A")); service.post(DefaultHandler("B")); service.post(DefaultHandler("C")); service.post(DefaultHandler("D")); std::cout << "asio poll" << std::endl; service.poll(); return 0; } // Output: asio poll { N ...calling poll again... { A } { B } { C } { D } } ```

Original source