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

djf*_*djf 27 c++ boost boost-asio

我正在考虑在我们目前使用的现有专有第三方网络协议之上编写自定义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.hpp.Dir_monitor_impl处理与inofity的实际交互,这是阻塞,因此在后台线程中运行.我同意这一点.但是它basic_dir_monitor_service也有一个内部工作线程,所有似乎正在做的就是在主要io_service和主要之间移动请求dir_monitor_impl.我玩了代码,删除了工作线程,basic_dir_monitor_service然后将请求直接发送到主io_service,程序仍像以前一样运行.

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

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

我应该提一下,我正在为功能不足的单核嵌入式系统编写软件.将这些额外的线程放在适当的位置似乎会强加不必要的上下文切换,如果可能的话我想避免这种切换.

Tan*_*ury 27

总之,一致性.这些服务试图满足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已经结束.因此,消息无法通过logger_service::log()内部记录shutdown_service(),因为它会尝试将内部处理程序发布到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.
    
    Run Code Online (Sandbox Code Playgroud)

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

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

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


目录监视器示例

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

有关详细信息,当通过dir_monitor::async_monitor()a 启动异步监视器操作时,会将a 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();
Run Code Online (Sandbox Code Playgroud)

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

Asio Logger服务

使用内部线程和io_service:

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

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


    由于用户的生命周期io_service已结束,其事件队列既未被处理也无法发布其他处理程序.通过拥有自己的内部io_service,由自己的线程处理,logger_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();
Run Code Online (Sandbox Code Playgroud)

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