当boost :: asio :: io_service运行方法阻塞/取消阻塞时感到困惑


88

作为Boost.Asio的初学者,我对感到困惑io_service::run()。如果有人可以向我解释此方法的阻止/取消阻止,我将不胜感激。文档指出:

run()函数将阻塞,直到所有工作完成并且不再有要分派的处理程序,或者直到io_service停止为止。

多个线程可以调用该run()函数来建立线程池,线程池io_service可以从中执行处理程序。池中等待的所有线程都是等效的,并且io_service可以选择其中任何一个来调用处理程序。

从该run()函数正常退出意味着该io_service对象已停止(该stopped()函数返回true)。后续调用run()run_one()poll()poll_one()将除非有预先调用立即返回reset()

以下陈述是什么意思?

[...]不再派遣处理程序[...]


在尝试了解的行为时io_service::run(),我遇到了这个示例(示例3a)。在其中,我观察到这些io_service->run()障碍并等待工作订单。

// WorkerThread invines io_service->run()
void WorkerThread(boost::shared_ptr<boost::asio::io_service> io_service);
void CalculateFib(size_t);

boost::shared_ptr<boost::asio::io_service> io_service(
    new boost::asio::io_service);
boost::shared_ptr<boost::asio::io_service::work> work(
   new boost::asio::io_service::work(*io_service));

// ...

boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
  worker_threads.create_thread(boost::bind(&WorkerThread, io_service));
}

io_service->post( boost::bind(CalculateFib, 3));
io_service->post( boost::bind(CalculateFib, 4));
io_service->post( boost::bind(CalculateFib, 5));

work.reset();
worker_threads.join_all();

但是,在下面的代码中,客户端使用TCP / IP连接,并且run方法将阻塞,直到异步接收数据为止。

typedef boost::asio::ip::tcp tcp;
boost::shared_ptr<boost::asio::io_service> io_service(
    new boost::asio::io_service);
boost::shared_ptr<tcp::socket> socket(new tcp::socket(*io_service));

// Connect to 127.0.0.1:9100.
tcp::resolver resolver(*io_service);
tcp::resolver::query query("127.0.0.1", 
                           boost::lexical_cast< std::string >(9100));
tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
socket->connect(endpoint_iterator->endpoint());

// Just blocks here until a message is received.
socket->async_receive(boost::asio::buffer(buf_client, 3000), 0,
                      ClientReceiveEvent);
io_service->run();

// Write response.
boost::system::error_code ignored_error;
std::cout << "Sending message \n";
boost::asio::write(*socket, boost::asio::buffer("some data"), ignored_error);

run()在下面的两个示例中对其描述其行为的任何解释将不胜感激。

Answers:


234

基础

让我们从一个简化的示例开始,并研究相关的Boost.Asio片段:

void handle_async_receive(...) { ... }
void print() { ... }

...  

boost::asio::io_service io_service;
boost::asio::ip::tcp::socket socket(io_service);

...

io_service.post(&print);                             // 1
socket.connect(endpoint);                            // 2
socket.async_receive(buffer, &handle_async_receive); // 3
io_service.post(&print);                             // 4
io_service.run();                                    // 5

什么是处理程序

一个处理程序无非是一个回调。在示例代码中,有3个处理程序:

  • print处理程序(1)。
  • handle_async_receive处理程序(3)。
  • print处理程序(4)。

即使print()两次使用相同的功能,也要考虑每次使用都会创建自己的唯一可识别的处理程序。处理程序可以有多种形状和大小,范围从上述基本功能到更复杂的构造,例如从boost::bind()和lambda生成的函子。不管复杂程度如何,处理程序仍然只不过是回调。

什么是工作

工作是Boost.Asio代表应用程序代码执行的一些处理。有时Boost.Asio可能会在得知某项工作后立即开始进行某些工作,而有时它可能等待稍后的工作。完成工作后,Boost.Asio将通过调用提供的处理程序来通知应用程序

Boost.Asio的保证了处理器将只当前调用线程中运行run()run_one()poll(),或poll_one()。这些是将起作用并调用处理程序的线程。因此,在上面的示例中,print()将其发布到io_service(1)中时不会被调用。而是将其添加到中io_service,并将在以后的某个时间点调用。在这种情况下,它在io_service.run()(5)之内。

什么是异步操作?

一个异步操作创建工作,Boost.Asio的将调用处理程序通知应用程序时的工作已经完成。异步操作是通过调用名称带有前缀的函数来创建的async_。这些功能也称为启动功能

异步操作可以分解为三个独特的步骤:

  • 启动或通知相关的io_service工作需要完成。该async_receive操作(3)通知io_service,它需要异步读取数据从插座,然后async_receive立即返回。
  • 做实际的工作。在这种情况下,当socket接收数据时,字节将被读取并复制到中buffer。实际工作将通过以下任一方式完成:
    • 启动函数(3),如果Boost.Asio可以确定它不会阻塞。
    • 当应用程序显式运行时io_service(5)。
  • 调用handle_async_receive ReadHandler。同样,处理程序仅在运行的线程内调用io_service。因此,无论工作何时完成(3或5),都可以保证handle_async_receive()仅在io_service.run()(5)内调用。

这三个步骤之间在时间和空间上的分离称为控制流反转。这是使异步编程变得困难的复杂性之一。但是,有些技术可以帮助缓解这种情况,例如使用协程

怎么io_service.run()办?

当线程调用时io_service.run(),将从该线程内调用work和handlers。在上面的示例中,io_service.run()(5)将一直阻塞,直到:

  • 它已从两个print处理程序调用并返回,接收操作成功或失败,并且handle_async_receive已调用并返回了其处理程序。
  • 通过io_service明确停止了io_service::stop()
  • 从处理程序中引发异常。

一种潜在的伪流可以描述如下:

创建io_service
创建套接字
将打印处理程序添加到io_service(1)
等待套接字连接(2)
向io_service添加异步读取工作请求(3)
将打印处理程序添加到io_service(4)
运行io_service(5)
  有工作或管理人员吗?
    是的,有1个工作和2个处理程序
      套接字有数据吗?不,什么都不做
      运行打印处理程序(1)
  有工作或管理人员吗?
    是的,有1个工作和1个处理程序
      套接字有数据吗?不,什么都不做
      运行打印处理程序(4)
  有工作或管理人员吗?
    是的,有1件作品
      套接字有数据吗?不,继续等待
  -套接字接收数据-
      套接字有数据,将其读入缓冲区
      将handle_async_receive处理程序添加到io_service
  有工作或管理人员吗?
    是的,有一个处理程序
      运行handle_async_receive处理程序(3)
  有工作或管理人员吗?
    否,将io_service设置为stopped然后返回

请注意如何当读完成后,它增加了一个处理程序io_service。这个微妙的细节是异步编程的重要功能。它允许将处理程序链接在一起。例如,如果handle_async_receive未获得其期望的所有数据,则其实现可能会发布另一个异步读取操作,从而导致io_service工作量增加,因此不会从返回io_service.run()

当待办事项io_service具有跑出的工作,应用程序必须reset()io_service运行之前重新。


示例问题和示例3a代码

现在,让我们检查问题中引用的两段代码。

问题代码

socket->async_receive将工作添加到io_service。因此,io_service->run()它将阻塞,直到读取操作成功完成或出现错误,并且ClientReceiveEvent完成运行或引发异常为止。

示例3a代码

为了使它更容易理解,下面是一个较小的带注释的示例3a:

void CalculateFib(std::size_t n);

int main()
{
  boost::asio::io_service io_service;
  boost::optional<boost::asio::io_service::work> work =       // '. 1
      boost::in_place(boost::ref(io_service));                // .'

  boost::thread_group worker_threads;                         // -.
  for(int x = 0; x < 2; ++x)                                  //   :
  {                                                           //   '.
    worker_threads.create_thread(                             //     :- 2
      boost::bind(&boost::asio::io_service::run, &io_service) //   .'
    );                                                        //   :
  }                                                           // -'

  io_service.post(boost::bind(CalculateFib, 3));              // '.
  io_service.post(boost::bind(CalculateFib, 4));              //   :- 3
  io_service.post(boost::bind(CalculateFib, 5));              // .'

  work = boost::none;                                         // 4
  worker_threads.join_all();                                  // 5
}

在较高级别,程序将创建2个线程来处理io_service的事件循环(2)。这样就产生了一个简单的线程池,该线程池将计算斐波纳契数(3)。

问题代码与此代码之间的主要区别在于,该代码实际工作和处理程序添加到(3)之前调用io_service::run()(2 )。为了防止立刻返回,创建了一个对象(1)。这个对象可以防止工作用尽。因此,不会由于没有工作而返回。io_serviceio_service::run()io_service::workio_serviceio_service::run()

总体流程如下:

  1. 创建并添加添加到中的io_service::work对象io_service
  2. 创建了调用的线程池io_service::run()。这些工作线程不会io_service因为io_service::work对象而从中返回。
  3. 将3个计算斐波纳契数的处理程序添加到中io_service,并立即返回。工作线程而不是主线程可以立即开始运行这些处理程序。
  4. 删除io_service::work对象。
  5. 等待工作线程完成运行。这将仅在所有3个处理程序完成执行后发生,因为这三个处理程序都io_service没有工作。

可以使用与原始代码相同的方式来编写不同的代码,在原始代码中,将处理程序添加到中io_service,然后io_service处理事件循环。这样就无需使用io_service::work,从而产生以下代码:

int main()
{
  boost::asio::io_service io_service;

  io_service.post(boost::bind(CalculateFib, 3));              // '.
  io_service.post(boost::bind(CalculateFib, 4));              //   :- 3
  io_service.post(boost::bind(CalculateFib, 5));              // .'

  boost::thread_group worker_threads;                         // -.
  for(int x = 0; x < 2; ++x)                                  //   :
  {                                                           //   '.
    worker_threads.create_thread(                             //     :- 2
      boost::bind(&boost::asio::io_service::run, &io_service) //   .'
    );                                                        //   :
  }                                                           // -'
  worker_threads.join_all();                                  // 5
}

同步与异步

尽管问题中的代码正在使用异步操作,但是由于它正在等待异步操作完成,因此它可以有效地同步运行:

socket.async_receive(buffer, handler)
io_service.run();

等效于:

boost::asio::error_code error;
std::size_t bytes_transferred = socket.receive(buffer, 0, error);
handler(error, bytes_transferred);

作为一般经验法则,请尽量避免混合使用同步操作和异步操作。通常,它可以将一个复杂的系统变成一个复杂的系统。该答案强调了异步编程的优点,Boost.Asio文档中也介绍了其中的一些优点。


13
很棒的帖子。我只想添加一件事,因为我觉得它没有引起足够的关注:run()返回之后,您需要在io_service上调用reset(),然后才能再次运行()。否则,无论是否有async_操作正在等待,它都可能立即返回。
DeVadder

缓冲区来自哪里?它是什么?
ruipacheco 2015年

我还是很困惑。如果不建议混合为同步并且不建议使用异步,那么纯异步模式是什么?您可以举一个显示没有io_service.run();的代码的示例吗?
2015年

@Splash One可以io_service.poll()用来处理事件循环,而不会阻塞未完成的操作。避免混合同步操作和异步操作的主要建议是避免增加不必要的复杂性,并在处理程序需要很长时间才能完成时防止响应性差。在某些情况下它是安全的,例如当您知道同步操作不会阻塞时。
Tanner Sansbury,2015年

“ Boost.Asio确保处理程序仅在当前正在调用run()....的线程中运行”中的“当前”是什么意思?如果有N个线程(称为run()),那么哪个是“当前”线程?可以有很多吗?还是意味着已经完成执行async_*()(say async_read)的线程也可以保证调用其处理程序?
纳瓦兹

18

为了简化run操作,将其视为必须处理一堆纸的员工;它需要一张纸,执行纸页上告诉的内容,将纸页扔掉并拿下另一张纸;当他的床单用完时,它离开了办公室。在每张纸上可以有任何种类的说明,甚至可以在堆中添加新的纸。回到ASIO:你可以给一个io_service工作在两个方面,主要有:通过使用post它作为你的联系,在样品中或通过使用内部调用其他物体postio_service,比如socketasync_*方法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.