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

Trying to understand Boost.Asio custom service implementation

本文关键字:自定义 服务 实现 Asio Boost      更新时间:2023-10-16

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

根据Highscore Asio指南,您需要实现三个类来创建自定义Asio服务:

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

网络协议实现已经提供异步操作,并具有(阻塞)事件循环。所以我想,我应该把它放在我的服务实现类中,并在内部工作线程中运行事件循环。到目前为止还不错。

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

  1. Highscore页面提供了一个目录监视器示例。它本质上是对inotify的包装。有趣的类是inotify/basic_dir_monitor_service.hppinotify/dir_monitor_impl.hppDir_monitor_impl处理与inofity的实际交互,inofity是阻塞的,因此在后台线程中运行。我同意这一点。但basic_dir_monitor_service也有一个内部工作线程,所要做的似乎只是在main的io_servicedir_monitor_impl之间搅乱请求。我反复处理了代码,删除了basic_dir_monitor_service中的工作线程,而是将请求直接发布到主io_service,程序仍然像以前一样运行。

  2. 在Asio的自定义记录器服务示例中,我注意到了同样的方法。logger_service生成一个内部工作线程来处理日志记录请求。我还没有时间处理这些代码,但我认为,应该也可以将这些请求直接发布到主io_service。

拥有这些"中介工作者"有什么好处?你不能一直把所有的工作都发布到主io_service吗?我是否错过了Proactor模式的一些关键方面?

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

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

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

  • 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本身:

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


目录监视器示例

在目录监视器示例中,内部线程用于防止在等待事件时无限期地阻塞用户的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,那么user_handler()将不会被调用,直到dir_monitordir_name目录上观察到事件为止。因此,dir_monitor服务的实现不会以大多数用户期望的其他服务的一致方式运行。

Asio Logger服务

内部线程和io_service:的使用

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

    1. 关闭其每个服务
    2. 销毁io_service或其任何关联的strand中计划延迟调用的所有未调用处理程序
    3. 销毁其每个服务


    由于用户的io_service的生存期已结束,因此既不处理其事件队列,也无法发布其他处理程序。通过具有由其自己的线程处理的其自己的内部io_servicelogger_service使其他服务能够在其shutdown_service()期间记录消息。


其他注意事项

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

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

对于最后两点,dir_monitorI/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的更改。