Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

boost-asio

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 }  } 
like image 816
Prismatic Avatar asked Feb 21 '15 22:02

Prismatic


2 Answers

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().

like image 142
Tanner Sansbury Avatar answered Oct 16 '22 00:10

Tanner Sansbury


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

Sytaxically, this is valid. But, its not good bacause in every handler you should run poll(). Also, your stack trace will grow to very big sizes, and you can get big problems with the stack.

like image 45
PSIAlt Avatar answered Oct 16 '22 00:10

PSIAlt