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

时间:2015-10-16 02:32:44

标签: c++ asynchronous boost boost-asio

我在Boost ASIO文档中阅读的所有内容以及StackOverflow上的内容都表明我可以通过调用acceptor套接字上的async_accept来停止close操作。但是,当我尝试执行此操作时,not_socket处理程序中出现间歇async_accept错误。我做错了什么或者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 1}}(Boost版本1.59)。这是完整的电话:

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

Microsoft的documentation for setsockoptSO_UPDATE_ACCEPT_CONTEXT

  

使用侦听套接字的上下文更新接受套接字。

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

我希望有人可以告诉我,我错了,并且有办法安全地关闭一个忙碌的接受者。

1 个答案:

答案 0 :(得分:4)

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

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

  

将接受新连接的套接字。对象的所有权由调用者保留,调用者必须保证它在调用处理程序之前有效。

特别是,对等套接字server_connfor循环中具有自动范围。当async_accept操作未完成时,循环可以开始新的迭代,导致server_conn被破坏并违反生命周期要求。考虑通过以下任一方式延长server_conn的生命周期:

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