对于boost io_service,在epoll_wait上只阻塞了一个线程?

时间:2016-10-31 08:20:14

标签: c++ multithreading c++11 boost-asio

我阅读了Boost ASIO的源代码,我想知道它只有一个线程可以调用epoll_wait(当然,如果我使用epoll reactor)。
我想找到关于调用epoll_wait的多个线程的解决方案,这可能会导致不同的线程同时对同一个套接字进行读取。 我读了一些关键代码如下:

// Prepare to execute first handler from queue.
      operation* o = op_queue_.front();
      op_queue_.pop();
      bool more_handlers = (!op_queue_.empty());

      if (o == &task_operation_)
      {
        task_interrupted_ = more_handlers;

        if (more_handlers && !one_thread_)
          wakeup_event_.unlock_and_signal_one(lock);
        else
          lock.unlock();

        task_cleanup on_exit = { this, &lock, &this_thread };
        (void)on_exit;

        // Run the task. May throw an exception. Only block if the operation
        // queue is empty and we're not polling, otherwise we want to return
        // as soon as possible.
        task_->run(!more_handlers, this_thread.private_op_queue);
      }

task_是epoll reactor,它将在运行中调用epoll_wait, 我想它可能只有一个线程可以调用它,因为只有一个" task_operation _"在op_queue_中,我是对的吗? 如果我想在多线程中使用epoll,或者我可以使用" EPOLLONESHOT"这样它就可以确保一个线程一次处理一个套接字。

2 个答案:

答案 0 :(得分:2)

只有一个线程会调用epoll_wait。一旦线程接收到描述符的事件通知,它就会将描述符解复用到运行io_service的所有线程。根据{{​​3}}:

  

主题:

     
      
  • 使用epoll进行多路分解是在其中一个调用io_service::run()io_service::run_one()io_service::poll()io_service::poll_one()的线程中执行的。
  •   

单个描述符将由执行I / O的单个线程处理。因此,在使用异步操作时,不会对给定套接字同时执行I / O.

答案 1 :(得分:0)

  • 第一种情况,就是当您使用单个io_service实例并从多个线程调用io_service::run方法时。

让我们看一下schduler::run函数(简化):

std::size_t scheduler::run(asio::error_code& ec)
{
  mutex::scoped_lock lock(mutex_);

  std::size_t n = 0;
  for (; do_run_one(lock, this_thread, ec); lock.lock())
    if (n != (std::numeric_limits<std::size_t>::max)())
      ++n;
  return n;
}

因此,在锁定的情况下,它会调用do_run_one方法,类似于:

std::size_t scheduler::do_run_one(mutex::scoped_lock& lock,
    scheduler::thread_info& this_thread,
    const asio::error_code& ec)
{
  while (!stopped_)
  {
    if (!op_queue_.empty())
    {
      // Prepare to execute first handler from queue.
      operation* o = op_queue_.front();
      op_queue_.pop();
      bool more_handlers = (!op_queue_.empty());

      if (o == &task_operation_)
      {
        task_interrupted_ = more_handlers;

        if (more_handlers && !one_thread_)
          wakeup_event_.unlock_and_signal_one(lock);
        else
          lock.unlock();

        task_cleanup on_exit = { this, &lock, &this_thread };
        (void)on_exit;

        task_->run(!more_handlers, this_thread.private_op_queue);
      }
      else
      {
        //......
      }
    }
    else
    {
      wakeup_event_.clear(lock);
      wakeup_event_.wait(lock);
    }
  }

  return 0;
}

代码的有趣部分是这些行:

if (more_handlers && !one_thread_)
  wakeup_event_.unlock_and_signal_one(lock);
else
  lock.unlock(); 

我们现在讨论的情况是具有多个线程的情况,因此第一个条件将满足(假设我们在op_queue _中有相当多的待处理任务)。

wakeup_event_.unlock_and_signal_one最终做的是释放/解锁lock并通知正在等待条件等待的线程之一。所以,有了这个,至少另一个线程(无论谁获得锁定)现在可以调用do_run_one

您所说的task_正如您所说的epoll_reactor。并且,在它的run方法中,它调用epoll_wait(不保留lock_的{​​{1}}。

这里有趣的是当它迭代scheduler返回的所有准备好的描述符时它会做什么。它将它们推回到它在参数中作为参考接收的操作队列中。现在推送的操作的运行时类型为epoll_wait,而不是descriptor_state

task_operation_

因此,在for (int i = 0; i < num_events; ++i) { void* ptr = events[i].data.ptr; if (ptr == &interrupter_) { // don't call work_started() here. This still allows the scheduler to // stop if the only remaining operations are descriptor operations. descriptor_state* descriptor_data = static_cast<descriptor_state*>(ptr); descriptor_data->set_ready_events(events[i].events); ops.push(descriptor_data); } } 内的while循环的下一次迭代中,对于已完成的任务,它将命中scheduler::do_run_one分支(我之前在我的粘贴中省略了它):

else

调用 else { std::size_t task_result = o->task_result_; if (more_handlers && !one_thread_) wake_one_thread_and_unlock(lock); else lock.unlock(); // Ensure the count of outstanding work is decremented on block exit. work_cleanup on_exit = { this, &lock, &this_thread }; (void)on_exit; // Complete the operation. May throw an exception. Deletes the object. o->complete(this, ec, task_result); return 1; } 函数指针,它可能会调用用户传递给completeasync_read API的句柄。

  • 第二种情况,是您创建async_write个对象池并在1个或多个线程上调用其io_service方法的位置,即runio_service之间的映射可能是适合您的应用的1:1或1:N。这样,您可以循环方式将thread对象分配给io_service对象。

现在,回答你的问题:

  

如果我想在多线程中使用epoll,或者我可以使用“EPOLLONESHOT”   这样它就可以确保一个线程一次处理一个套接字。

如果我理解正确,你想使用1个线程处理套接字的所有事件?我认为这可以通过遵循方法2来实现,即创建一个soucket对象池并将其映射到1个线程。通过这种方式,您可以确保特定套接字上的所有活动只能由一个线程处理,即io_service上的线程。

您无需担心在上述情况下设置io_service:run

我不太确定使用第一种方法获得相同的行为,第一种方法是多线程和1 EPOLLONESHOT

但是,如果您根本不使用线程,即您的io_service在单线程上运行,那么您不必担心所有这些,因为asio的所有目的都是抽象所有这些东西。