io_service::p oll_one 非确定性行为

io_service::poll_one non-deterministic behaviour

本文关键字:非确定 性行为 one service io oll      更新时间:2023-10-16

在下面的代码中,我希望输出始终为 1,因为我预计在调用 poll_one() 时只有一个处理程序运行。但是,大约 300 次一次,输出实际上是 3。根据我对提升库的理解,这似乎是不正确的。非确定性行为是错误还是预期?

#include <boost/asio.hpp>
int main() {
  boost::asio::io_service io;
  boost::asio::io_service::work io_work(io);
  boost::asio::io_service::strand strand1(io);
  boost::asio::io_service::strand strand2(io);
  int val = 0;
  strand1.post([&val, &strand2]() {
    val = 1;
    strand2.post([&val]() {
      val = 2;
    });
    boost::asio::spawn(strand2, [&val](boost::asio::yield_context yield) {
      val = 3;
    });
  });
  io.poll_one();
  std::cout << "Last executed: " << val << std::endl;
  return 0;
}

使用 boost-asio 1.60.0.6

观察到的行为定义明确,预计会发生,但不应期望它经常发生。

Asio 的链实现池有限,链的默认分配策略是散列。 如果发生哈希冲突,两条链将使用相同的实现。 发生哈希冲突时,该示例简化为以下演示:

#include <cassert>
#include <boost/asio.hpp>
int main()
{
  boost::asio::io_service io_service;
  boost::asio::io_service::strand strand1(io_service);
  // Have strand2 use the same implementation as strand1.
  boost::asio::io_service::strand strand2(strand1);
  int value = 0;
  auto handler1 = [&value, &strand1, &strand2]() {
    assert(strand1.running_in_this_thread());
    assert(strand2.running_in_this_thread());
    value = 1;
    // handler2 is queued into strand and never invoked.
    auto handler2 = [&value]() { assert(false); };
    strand2.post(handler2);
    // handler3 is immediately executed.
    auto handler3 = [&value]() { value = 3; };
    strand2.dispatch(handler3);
    assert(value == 3);
  };
  // Enqueue handler1.
  strand1.post(handler1);
  // Run the event processing loop, executing handler1.
  assert(io_service.poll_one() == 1);
}

在上面的例子中:

  • io_service.poll_one()执行单个就绪处理程序 ( handler1 (
  • 从不调用handler2
  • handler3strand2.dispatch() 内立即调用,因为strand2.dispatch()是从处理程序中调用的,strand2.running_in_this_thread()返回true

有各种细节会导致观察到的行为:

  • io_service::poll_one()将运行io_service的事件循环,并且没有阻塞,它将最多执行一个准备运行的处理程序。 在dispatch()上下文中立即执行的处理程序永远不会排队进入io_service,并且不受poll_one()调用单个处理程序的限制。

  • boost::asio::spawn(strand, function)重载启动堆叠协程,如下所示strand.dispatch()

    • 如果 strand.running_in_this_thread() 返回调用方的false,则协程将发布到strand以进行延迟调用
    • 如果strand.running_in_this_thread()返回调用方的true,则协程将立即执行
  • 使用相同实现的离散strand对象仍保持链的保证。 也就是说,不会发生并发执行,并且处理程序调用的顺序已明确定义。 当离散strand对象使用离散实现,并且多个线程正在运行io_service时,可以观察到离散链并发执行。 但是,当离散strand对象使用相同的实现时,即使多个线程正在运行io_service,也不会观察到并发性。 记录了此行为:

    该实现不保证通过不同链对象发布或调度的处理程序将被并发调用。

  • Asio有一个有限的链实现池。 当前默认值为 193,可以通过将BOOST_ASIO_STRAND_IMPLEMENTATIONS定义为所需数字来控制。 此功能在 Boost.Asio 1.48 发行说明中有所说明

    通过将BOOST_ASIO_STRAND_IMPLEMENTATIONS定义为所需数量,使链实现的数量可配置。

    通过减小池大小,可以增加两个离散链使用相同的实现的机会。 使用原始代码,如果要将池大小设置为 1 ,则 strand1strand2 将始终使用相同的实现,从而导致val始终3(演示(。

  • 分配链实现的默认策略是使用黄金比率哈希。 使用散列算法时,可能会发生冲突,从而导致对多个离散strand对象使用相同的实现。 通过定义BOOST_ASIO_ENABLE_SEQUENTIAL_STRAND_ALLOCATION,可以将分配策略更改为循环,防止发生冲突,直到BOOST_ASIO_STRAND_IMPLEMENTATIONS + 1链分配发生。 此功能在 Boost.Asio 1.48 发行说明中有所说明:

    添加了对新 BOOST_ASIO_ENABLE_SEQUENTIAL_STRAND_ALLOCATION 标志的支持,该标志将链实现的分配切换为使用循环方法而不是哈希。

鉴于上述详细信息,在原始代码中观察到1时,将发生以下情况:

  • strand1strand2具有离散的实现
  • io_service::poll_one()执行直接发布到strand1的单个处理程序
  • 发布到strand1集合中的处理程序val 1
  • 发布到 strand2 中的处理程序已排队且永远不会调用
  • 协程的创建是延迟的,因为strand 的调用顺序保证会阻止在发布到 strand2 的上一个处理程序执行之前创建协程:

    给定一个链对象s,如果s.post(a)发生在s.dispatch(b)之前,后者在链外执行,那么asio_handler_invoke(a1, &a1)发生在asio_handler_invoke(b1, &b1)之前。

另一方面,当观察到3时:

  • strand1strand2发生哈希冲突,导致它们使用相同的底层链实现
  • io_service::poll_one()执行直接发布到strand1的单个处理程序
  • 发布到strand1集中的处理程序val 1
  • 发布到 strand2 中的处理程序已排队且从未调用
  • 协程立即在boost::asio::spawn()内创建和调用,val设置为3,因为strand2可以安全地执行协程,同时保持非并发执行和处理程序调用顺序的保证