试图了解 Boost.Asio 自定义服务实现

Posted

技术标签:

【中文标题】试图了解 Boost.Asio 自定义服务实现【英文标题】:Trying to understand Boost.Asio custom service implementation 【发布时间】:2014-07-16 05:26:33 【问题描述】:

我正在考虑在我们目前使用的现有专有第 3 方网络协议之上编写自定义 Asio 服务。

根据Highscore Asio guide需要实现三个类来创建自定义的Asio服务:

派生自 boost::asio::basic_io_object 的类,表示新的 I/O 对象。 派生自boost::asio::io_service::service 的类,表示向 I/O 服务注册并可从 I/O 对象访问的服务。 不是从代表服务实现的任何其他类派生的类。

网络协议实现已经提供了异步操作并且有一个(阻塞的)事件循环。所以我想,我会把它放到我的服务实现类中,并在内部工作线程中运行事件循环。到目前为止一切顺利。

查看自定义服务的一些示例,我注意到服务类产生了它们自己的内部线程(实际上它们实例化了它们自己的内部 io_service 实例)。例如:

    高分页面提供directory monitor example。它本质上是一个围绕 inotify 的包装器。有趣的类是inotify/basic_dir_monitor_service.hppinotify/dir_monitor_impl.hppDir_monitor_impl 处理与 inofity 的实际交互,这是阻塞的,因此在后台线程中运行。我同意这一点。但是basic_dir_monitor_service 也有一个内部工作线程,似乎正在做的只是在主的io_servicedir_monitor_impl 之间混洗请求。我修改了代码,删除了basic_dir_monitor_service 中的工作线程,而是直接将请求发布到主 io_service 并且程序仍然像以前一样运行。

    在 Asio 的 custom logger service example 中,我注意到了相同的方法。 logger_service 产生一个内部工作线程来处理日志记录请求。我没有时间玩那些代码,但我认为应该可以将这些请求直接发布到主 io_service。

拥有这些“中介工人”有什么好处?您不能一直将所有工作都发布到主 io_service 吗?我是否遗漏了 Proactor 模式的一些关键方面?

我可能应该提到我正在为一个功率不足的单核嵌入式系统编写软件。拥有这些额外的线程似乎只是强加了不必要的上下文切换,我希望尽可能避免。

【问题讨论】:

很棒的问题。这些天我打算尝试那个特定的例子。很高兴看到其他人与之对战。稍后阅读 @sehe 来自你的哇,这意味着什么。我在 SO 上多次注意到你的句柄。通常会回答一些令人费解的 Boost.Spirit 问题 关于 inotify 的一个注意事项:它不必阻塞。事实上,它经常与 epoll 或 select 或类似的一起使用。 @John 是的,这是真的! inotify 页面说这是可能的,但建议不要这样做……但他们知道什么:) 所以,理论上应该也可以放弃该线程? 它在哪里建议不要使用非阻塞?我在我检查的参考文献中没有看到类似的东西。 【参考方案1】:

简而言之,一致性。这些服务试图满足 Boost.Asio 提供的服务提出的用户期望。

使用内部io_service 可以明确分离处理程序的所有权和控制权。如果自定义服务将其内部处理程序发布到用户的io_service,则服务的内部处理程序的执行将与用户的处理程序隐式耦合。通过Boost.Asio Logger Service 示例考虑这将如何影响用户期望:

logger_service 在处理程序中写入文件流。因此,从不处理 io_service 事件循环的程序(例如仅使用同步 API 的程序)将永远不会写入日志消息。 logger_service 将不再是线程安全的,如果 io_service 由多个线程处理,则可能会调用未定义的行为。 logger_service 的内部操作的生命周期受到io_service 的限制。例如,当调用服务的shutdown_service() 函数时,拥有io_service 的生命周期已经结束。因此,无法通过shutdown_service() 中的logger_service::log() 记录消息,因为它会尝试将内部处理程序发布到生命周期已经结束的io_service

用户可能不再假定操作和处理程序之间存在一对一的映射。例如:

boost::asio::io_service io_service;
debug_stream_socket socket(io_service);
boost::asio::async_connect(socket, ..., &connect_handler);
io_service.poll();
// Can no longer assume connect_handler has been invoked.

在这种情况下,io_service.poll() 可能会调用logger_service 内部的处理程序,而不是connect_handler()

此外,这些内部线程试图模仿 Boost.Asio itself 内部使用的行为:

此库在特定平台上的实现可能会使用一个或多个内部线程来模拟异步性。这些线程必须尽可能对库用户不可见。


Directory Monitor example

在目录监视器示例中,内部线程用于防止在等待事件时无限期地阻塞用户的io_service。一旦事件发生,完成处理程序就可以被调用,因此内部线程将用户处理程序发布到用户的io_service 以进行延迟调用。此实现使用对用户几乎不可见的内部线程来模拟异步性。

有关详细信息,当通过dir_monitor::async_monitor() 启动异步监视器操作时,basic_dir_monitor_service::monitor_operation 将发布到内部io_service。调用时,此操作会调用 dir_monitor_impl::popfront_event(),这是一个潜在的阻塞调用。因此,如果monitor_operation 被发布到用户的io_service,用户的线程可能会被无限期地阻塞。考虑对以下代码的影响:

boost::asio::io_service io_service;
boost::asio::dir_monitor dir_monitor(io_service); 
dir_monitor.add_directory(dir_name); 
// Post monitor_operation into io_service.
dir_monitor.async_monitor(...);
io_service.post(&user_handler);
io_service.run();

在上面的代码中,如果io_service.run()首先调用monitor_operation,那么直到dir_monitor观察到dir_name目录上的事件,user_handler()才会被调用。因此,dir_monitor 服务的实现不会以大多数用户期望其他服务的一致方式运行。

Asio Logger Service

使用内螺纹和io_service

通过在内部线程中执行潜在的阻塞或昂贵的调用来减少登录用户线程的开销。 保证std::ofstream 的线程安全,因为只有单个内部线程写入流。如果日志记录是直接在logger_service::log() 中完成的,或者如果logger_service 将其处理程序发布到用户的io_service 中,那么线程安全将需要显式同步。其他同步机制可能会给实现带来更大的开销或复杂性。

允许servicesshutdown_service() 中记录消息。在destruction 期间,io_service 将:

    关闭其每项服务。 在io_service 或其任何关联的strands 中销毁所有计划延迟调用的未调用处理程序。 销毁其每个服务。

由于用户io_service 的生命周期已经结束,它的事件队列既没有被处理,也不能发布额外的处理程序。通过拥有自己的内部io_service(由其自己的线程处理),logger_service 使其他服务能够在其shutdown_service() 期间记录消息。


其他注意事项

在实现自定义服务时,需要考虑以下几点:

阻止内部线程上的所有信号。 永远不要直接调用用户的代码。 如何在实现被销毁时跟踪和发布用户处理程序。 服务拥有的资源在服务的实现之间共享。

对于最后两点,dir_monitor I/O 对象表现出用户可能没有预料到的行为。由于服务中的单个线程在单个实现的事件队列上调用阻塞操作,它有效地阻塞了可能立即完成的操作:

boost::asio::io_service io_service;
boost::asio::dir_monitor dir_monitor1(io_service); 
dir_monitor1.add_directory(dir_name1); 
dir_monitor1.async_monitor(&handler_A);

boost::asio::dir_monitor dir_monitor2(io_service); 
dir_monitor2.add_directory(dir_name2); 
dir_monitor2.async_monitor(&handler_B);
// ... Add file to dir_name2.


  // Use scope to enforce lifetime.
  boost::asio::dir_monitor dir_monitor3(io_service); 
  dir_monitor3.add_directory(dir_name3); 
  dir_monitor3.async_monitor(&handler_C);

io_service.run();

虽然与handler_B()(成功)和handler_C()(中止)相关的操作不会阻塞,但basic_dir_monitor_service 中的单个线程被阻塞,等待对dir_name1 的更改。

【讨论】:

以上是关于试图了解 Boost.Asio 自定义服务实现的主要内容,如果未能解决你的问题,请参考以下文章

boost::asio::async_read_until 与自定义匹配条件运算符重载混淆

boost asio tcp 收发教程

使用Boost asio实现同步的TCP/IP通信

boost::asio 中的 NAT 打孔

使用 Boost.Asio 获取本地 IP 地址

boost::asio::read() 永远阻塞