io_service :: poll_one非确定性行为

Mic*_*rek 13 c++ boost boost-asio

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

#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;
}
Run Code Online (Sandbox Code Playgroud)

使用boost-asio 1.60.0.6

Tan*_*ury 14

观察到的行为已经明确定义并且预计会发生,但人们不应该预期它会经常发生.

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

#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);
}
Run Code Online (Sandbox Code Playgroud)

在上面的例子中:

  • io_service.poll_one()执行一个就绪处理程序(handler1)
  • handler2 永远不会被调用
  • handler3在内部调用strand2.dispatch(),就像strand2.dispatch()strand2.running_in_this_thread()返回的处理程序中调用一样true

有各种细节有助于观察到的行为:

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

  • boost::asio::spawn(strand, function)过载开始stackful协程作为假设通过strand.dispatch():

    • 如果strand.running_in_this_thread()返回false调用者,那么协程将被发布到strandfor deferred调用中
    • 如果strand.running_in_this_thread()返回true调用者,则会立即执行协程
  • strand使用相同实现的离散对象仍然保持链的保证.即,不会发生并发执行,并且很好地定义了处理程序调用顺序.当离散strand对象使用离散实现,并且多个线程正在运行时io_service,则可以观察到并行执行的离散链.但是,当离散strand对象使用相同的实现时,即使多个线程正在运行,也不会观察到并发性io_service.这种行为被记录:

    该实现不保证将同时调用通过不同strand对象发布或分派的处理程序.

  • 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集合val1
  • 发布到的处理程序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集合val1
  • 发布到的处理程序strand2已入队并且从未被调用过
  • 协同程序立即创建并调用boost::asio::spawn(),设置val3,strand2可以安全地执行协程,同时保持非并发执行的保证和处理程序调用的顺序

  • 这个答案是一件艺术品.我喜欢边缘案例的小型演示.+100 AFAIAC (4认同)