c++ - 如何安全地取消 Boost ASIO 异步接受操作?

标签 c++ asynchronous boost boost-asio

我在 Boost ASIO 文档和 StackOverflow 上阅读的所有内容都表明我可以通过在接受器套接字上调用 close 来停止 async_accept 操作。但是,当我尝试执行此操作时,async_accept 处理程序中出现间歇性的 not_socket 错误。是我做错了什么还是 Boost ASIO 不支持这个?

(相关问题:herehere。)

(注意:我在 Windows 7 上运行并使用 Visual Studio 2015 编译器。)

我面临的核心问题是接受传入连接的 async_accept 操作与我对 close 的调用之间的竞争条件。即使使用显式或隐式链时也会发生这种情况。

请注意我对 async_accept调用严格发生在我对close 的调用之前。我得出结论,竞争条件是在我调用 close 和 Boost ASIO 中接受传入连接的底层代码之间。

我已经包含了演示该问题的代码。该程序重复创建一个接受器,连接到它,然后立即关闭该接受器。它期望 async_accept 操作成功完成或被取消。任何其他错误都会导致程序中止,这是我间歇性看到的情况。

为了同步,程序使用显式链。尽管如此,对 close 的调用与 async_accept 操作的效果 不同步,因此有时接受器在接受传入连接之前关闭,有时它随后关闭,有时两者都不关闭——这就是问题所在。

代码如下:

#include <algorithm>
#include <boost/asio.hpp>
#include <cstdlib>
#include <future>
#include <iostream>
#include <memory>
#include <thread>

int main()
{
  boost::asio::io_service ios;
  auto work = std::make_unique<boost::asio::io_service::work>(ios);

  const auto ios_runner = [&ios]()
  {
    boost::system::error_code ec;
    ios.run(ec);
    if (ec)
    {
      std::cerr << "io_service runner failed: " << ec.message() << '\n';
      abort();
    }
  };

  auto thread = std::thread{ios_runner};

  const auto make_acceptor = [&ios]()
  {
    boost::asio::ip::tcp::resolver resolver{ios};
    boost::asio::ip::tcp::resolver::query query{
      "localhost",
      "",
      boost::asio::ip::resolver_query_base::passive |
      boost::asio::ip::resolver_query_base::address_configured};
    const auto itr = std::find_if(
      resolver.resolve(query),
      boost::asio::ip::tcp::resolver::iterator{},
      [](const boost::asio::ip::tcp::endpoint& ep) { return true; });
    assert(itr != boost::asio::ip::tcp::resolver::iterator{});
    return boost::asio::ip::tcp::acceptor{ios, *itr};
  };

  for (auto i = 0; i < 1000; ++i)
  {
    auto acceptor = make_acceptor();
    const auto saddr = acceptor.local_endpoint();

    boost::asio::io_service::strand strand{ios};
    boost::asio::ip::tcp::socket server_conn{ios};

    // Start accepting.
    std::promise<void> accept_promise;
    strand.post(
      [&]()
    {
      acceptor.async_accept(
        server_conn,
        strand.wrap(
          [&](const boost::system::error_code& ec)
          {
            accept_promise.set_value();
            if (ec.category() == boost::asio::error::get_system_category()
              && ec.value() == boost::asio::error::operation_aborted)
              return;
            if (ec)
            {
              std::cerr << "async_accept failed (" << i << "): " << ec.message() << '\n';
              abort();
            }
          }));
    });

    // Connect to the acceptor.
    std::promise<void> connect_promise;
    strand.post(
      [&]()
    {
      boost::asio::ip::tcp::socket client_conn{ios};
      {
        boost::system::error_code ec;
        client_conn.connect(saddr, ec);
        if (ec)
        {
          std::cerr << "connect failed: " << ec.message() << '\n';
          abort();
        }
        connect_promise.set_value();
      }
    });
    connect_promise.get_future().get();   // wait for connect to finish

    // Close the acceptor.
    std::promise<void> stop_promise;
    strand.post([&acceptor, &stop_promise]()
    {
      acceptor.close();
      stop_promise.set_value();
    });
    stop_promise.get_future().get();   // wait for close to finish
    accept_promise.get_future().get(); // wait for async_accept to finish
  }

  work.reset();
  thread.join();
}

这是示例运行的输出:

async_accept failed (5): An operation was attempted on something that is not a socket

括号中的数字表示程序运行了多少次成功的迭代。

更新 #1: 根据 Tanner Sansbury 的回答,我添加了一个 std::promise 来表示 async_accept 的完成处理程序。这对我看到的行为没有影响。

更新 #2: not_socket 错误源于对 setsockopt 的调用,来自 call_setsockopt,来自 socket_ops::setsockopt 文件 boost\asio\detail\impl\socket_ops.ipp(Boost 版本 1.59)。这是完整的调用:

socket_ops::setsockopt(new_socket, state,
  SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT,
  &update_ctx_param, sizeof(SOCKET), ec);

微软的documentation for setsockopt说到 SO_UPDATE_ACCEPT_CONTEXT:

Updates the accepting socket with the context of the listening socket.

我不确定这到底是什么意思,但听起来好像如果监听套接字关闭就会失败。这表明,在 Windows 上,无法安全地关闭当前正在为 async_accept 操作运行完成处理程序的接受器。

我希望有人能告诉我我错了,并且有一种方法可以安全地关闭繁忙的接受器。

最佳答案

示例程序不会取消async_accept操作。建立连接后,async_accept 操作将在内部发布以完成。此时,该操作不再可取消,并且不会受到 acceptor.close() 的影响。 .

观察到的问题是未定义行为的结果。该程序未能满足 async_accept 的生命周期要求的 peer 参数:

The socket into which the new connection will be accepted. Ownership of the peer object is retained by the caller, which must guarantee that it is valid until the handler is called.

特别是,对等套接字 server_connfor 循环中具有自 Action 用域。当 async_accept 操作未完成时,循环可能会开始新的迭代,导致 server_conn 被破坏并违反生命周期要求。考虑延长 server_conn 的生命周期:

  • 在接受处理程序中设置一个 std::future 并等待相关的 std::promise,然后再继续循环的下一次迭代
  • 通过智能指针管理 server_conn 并将所有权传递给接受处理程序

关于c++ - 如何安全地取消 Boost ASIO 异步接受操作?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33161640/

相关文章:

c++ - Arduino - 具有 pow(x,y) 函数的奇数指数行为

c++ - 传递一个带有 "this"作为参数的 shared_ptr 并将其存储在一个变量中

c++ - #include <boost/chrono.hpp> 导致无法解析的外部符号,使用了 bcp

c++ - 可以在没有 epsilon 的情况下将 float 与 0.0 进行比较吗?

c++ - 通过 C 绑定(bind)公开 C++ 库

c++ - 物理和 std::numeric_limits<double>::epsilon()...我们何时以及为何需要它?

c++ - 如何在 boost::filesystem 中使用 copy_file?

javascript - 调用 Resolve 函数转到 JavaScript Promise 中的 Reject 函数

c# - [C#][WPF]如何在不卡住UI的情况下制作异步TreeView?

node.js - 如何强制for循环等待回调在nodejs中完成?