将 boost::deadline_timer 回调与相应的 wait_async 相匹配

Sim*_*ott 1 c++ boost boost-asio

考虑这个简短的代码片段,其中一个 boost::deadline_timer 中断了另一个:

#include <iostream>
#include <boost/bind.hpp>
#include <boost/function.hpp>
#include <boost/asio.hpp>

static boost::asio::io_service io;
boost::asio::deadline_timer timer1(io);
boost::asio::deadline_timer timer2(io);

static void timer1_handler1(const boost::system::error_code& error)
{
    std::cout << __PRETTY_FUNCTION__ << " time:" << time(0) << " error:" << error.message() << " expect:Operation canceled." << std::endl;        
}        

static void timer1_handler2(const boost::system::error_code& error)
{
    std::cout << __PRETTY_FUNCTION__ << " time:" << time(0) << " error:" << error.message() << " expect:success." << std::endl;        
}        

static void timer2_handler1(const boost::system::error_code& error)
{
    std::cout << __PRETTY_FUNCTION__ << " time:" << time(0) << " error:" << error.message() << " expect:success." << std::endl;        
    std::cout << "cancel and restart timer1. Bind to timer1_handler2" << std::endl;
    timer1.cancel();
    timer1.expires_from_now(boost::posix_time::milliseconds(10000));
    timer1.async_wait(boost::bind(timer1_handler2, boost::asio::placeholders::error));        
}        

int main()
{
    std::cout << "Start timer1. Bind to timer1_handler1." << std::endl;
    timer1.expires_from_now(boost::posix_time::milliseconds(2000));
    timer1.async_wait(boost::bind(timer1_handler1, boost::asio::placeholders::error));        

    std::cout << "Start timer2. Bind to timer2_handler1. Will interrupt timer1." << std::endl;
    timer2.expires_from_now(boost::posix_time::milliseconds(2000));
    timer2.async_wait(boost::bind(timer2_handler1, boost::asio::placeholders::error));        

    std::cout << "Run the boost io service." << std::endl;
    io.run();

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

如果timer2的时间在2秒左右变化,有时timer1_handler1报告成功,有时操作被取消。在这个简单的例子中,这可能是确定的,因为我们知道 timer2 设置为什么时间。

./timer1
Start timer1. Bind to timer1_handler1.
Start timer2. Bind to timer2_handler1. Will interrupt timer1.
Run the boost io service.
void timer1_handler1(const boost::system::error_code&) time:1412680360 error:Success expect:Operation canceled.
void timer2_handler1(const boost::system::error_code&) time:1412680360 error:Success expect:success.
cancel and restart timer1. Bind to timer1_handler2
void timer1_handler2(const boost::system::error_code&) time:1412680370 error:Success expect:success.
Run Code Online (Sandbox Code Playgroud)

这代表了一个更复杂的系统,其中timer1 正在实现超时,而timer2 实际上是一个异步套接字。有时,我观察到这样的情况:timer1 取消得太晚,并且第一个处理程序在调用第二个 async_wait() 后返回,从而给出虚假超时。

显然,我需要将处理程序回调与相应的 async_wait() 调用相匹配。有一个方便的方法来做到这一点吗?

Tan*_*ury 5

解决所提出问题的一种便捷方法是使用官方 Boost超时示例中使用的方法,即管理由多个非链式异步操作组成的高级异步操作。在其中,处理程序通过检查当前状态来做出决策,而不是将处理程序逻辑与预期或提供的状态耦合。

在制定解决方案之前,确定处理程序执行的所有可能情况非常重要。运行时io_service,事件循环的单次迭代将执行准备运行的所有操作,并且在操作完成后,用户的完成处理程序将排队,并指示error_code操作的状态。然后将io_service调用排队的完成处理程序。因此,在单次迭代中,所有准备运行的操作都在完成处理程序之前以未指定的顺序执行,并且调用完成处理程序的顺序也未指定。例如,当async_read_with_timeout()async_read()和编写操作时async_wait(),其中任一操作仅在另一个操作的完成处理程序中取消,则可能出现以下情况:

  • async_read()运行且未async_wait()准备好运行,然后 async_read()的完成处理程序被调用并取消async_wait(),导致async_wait()的完成处理程序运行时出现错误boost::asio::error::operation_aborted
  • async_read()尚未准备好运行并async_wait()运行,然后async_wait()的完成处理程序被调用并取消async_read(),导致async_read()的完成处理程序运行时出现错误boost::asio::error::operation_aborted
  • async_read()async_wait()运行,然后async_read()的完成处理程序首先被调用,但async_wait()操作已经完成并且无法取消,因此async_wait()的完成处理程序将运行且不会出现错误。
  • async_read()async_wait()运行,然后async_wait()的完成处理程序首先被调用,但async_read()操作已经完成并且无法取消,因此async_read()的完成处理程序将运行且不会出现错误。

完成处理程序error_code指示操作的状态,并且不反映其他完成处理程序导致的状态更改;因此,当error_code成功时,可能需要检查当前状态以执行条件分支。然而,在引入额外的状态之前,值得花精力检查更高级别操作的目标以及已经可用的状态。对于此示例,我们定义的目标是,async_read_with_timeout()如果在截止日期之前尚未收到数据,则关闭套接字。对于状态,套接字要么打开,要么关闭;定时器提供过期时间;系统时钟提供当前时间。在检查了目标和可用的状态信息之后,人们可能会提出:

  • async_wait()如果计时器的当前到期时间已经过去,则处理程序应该仅关闭套接字。
  • async_read()的处理程序应该将计时器的到期时间设置为未来。

使用这种方法,如果async_read()的完成处理程序在 之前运行async_wait(),那么要么async_wait()将被取消,要么async_wait()的完成处理程序将不会关闭连接,因为当前的到期时间是将来的。另一方面,如果async_wait()的完成处理程序在 之前运行async_read(),则要么async_read()将被取消,要么 的async_read()完成处理程序可以检测到套接字已关闭。

这是一个完整的最小示例,演示了针对各种用例的这种方法:

#include <cassert>
#include <iostream>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/thread.hpp>

class client
{
public:

  // This demo is only using status for asserting code paths.  It is not
  // necessary nor should it be used for conditional branching.
  enum status_type
  {
    unknown,
    timeout,
    read_success,
    read_failure
  };

public:

  client(boost::asio::ip::tcp::socket& socket)
    : strand_(socket.get_io_service()),
      timer_(socket.get_io_service()),
      socket_(socket),
      status_(unknown)
  {}

  status_type status() const { return status_; }

  void async_read_with_timeout(boost::posix_time::seconds seconds)
  {
    strand_.post(boost::bind(
        &client::do_async_read_with_timeout, this, seconds));
  }

private:

  void do_async_read_with_timeout(boost::posix_time::seconds seconds)
  {
    // Start a timeout for the read.
    timer_.expires_from_now(seconds);
    timer_.async_wait(strand_.wrap(boost::bind(
        &client::handle_wait, this,
        boost::asio::placeholders::error)));

    // Start the read operation.
    boost::asio::async_read(socket_,  
        boost::asio::buffer(buffer_),
        strand_.wrap(boost::bind(
          &client::handle_read, this,
          boost::asio::placeholders::error,
          boost::asio::placeholders::bytes_transferred)));
  }

  void handle_wait(const boost::system::error_code& error)
  {
    // On error, such as cancellation, return early.
    if (error)
    {
      std::cout << "timeout cancelled" << std::endl;
      return;
    }

    // The timer may have expired, but it is possible that handle_read()
    // ran succesfully and updated the timer's expiration:
    // - a new timeout has been started.  For example, handle_read() ran and
    //   invoked do_async_read_with_timeout().
    // - there are no pending timeout reads.  For example, handle_read() ran
    //   but did not invoke do_async_read_with_timeout();
    if (timer_.expires_at() > boost::asio::deadline_timer::traits_type::now())
    {
      std::cout << "timeout occured, but handle_read ran first" << std::endl;
      return;
    }

    // Otherwise, a timeout has occured and handle_read() has not executed, so
    // close the socket, cancelling the read operation.
    std::cout << "timeout occured" << std::endl;
    status_ = client::timeout;
    boost::system::error_code ignored_ec;
    socket_.close(ignored_ec);
  }

  void handle_read(
    const boost::system::error_code& error,
    std::size_t bytes_transferred)
  {
    // Update timeout state to indicate handle_read() has ran.  This
    // cancels any pending timeouts.
    timer_.expires_at(boost::posix_time::pos_infin);

    // On error, return early.
    if (error)
    {
      std::cout << "read failed: " << error.message() << std::endl;
      // Only set status if it is unknown.
      if (client::unknown == status_) status_ = client::read_failure;
      return;
    }

    // The read was succesful, but if a timeout occured and handle_wait()
    // ran first, then the socket is closed, so return early.
    if (!socket_.is_open())
    {
      std::cout << "read was succesful but timeout occured" << std::endl;
      return;
    }

    std::cout << "read was succesful" << std::endl;
    status_ = client::read_success;
  }

private:

  boost::asio::io_service::strand strand_;
  boost::asio::deadline_timer timer_;
  boost::asio::ip::tcp::socket& socket_;
  char buffer_[1];
  status_type status_;
};

// This example is not interested in the connect handlers, so provide a noop
// function that will be passed to bind to meet the handler concept
// requirements.
void noop() {}

/// @brief Create a connection between the server and client socket.
void connect_sockets(
  boost::asio::ip::tcp::acceptor& acceptor,
  boost::asio::ip::tcp::socket& server_socket,
  boost::asio::ip::tcp::socket& client_socket)
{
  boost::asio::io_service& io_service = acceptor.get_io_service();
  acceptor.async_accept(server_socket, boost::bind(&noop));
  client_socket.async_connect(acceptor.local_endpoint(), boost::bind(&noop));
  io_service.reset();
  io_service.run();
  io_service.reset();
}

int main()
{
  using boost::asio::ip::tcp;
  boost::asio::io_service io_service;
  tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(), 0));

  // Scenario 1: timeout
  // The server writes no data, causing a client timeout to occur.
  {
    std::cout << "[Scenario 1: timeout]" << std::endl;
    // Create and connect I/O objects.
    tcp::socket server_socket(io_service);
    tcp::socket client_socket(io_service);
    connect_sockets(acceptor, server_socket, client_socket);

    // Start read with timeout on client.
    client client(client_socket);
    client.async_read_with_timeout(boost::posix_time::seconds(0));

    // Allow do_read_with_timeout to intiate actual operations.
    io_service.run_one();    

    // Run timeout and read operations.
    io_service.run();
    assert(client.status() == client::timeout);
  }

  // Scenario 2: no timeout, succesful read
  // The server writes data and the io_service is ran before the timer 
  // expires.  In this case, the async_read operation will complete and
  // cancel the async_wait.
  {
    std::cout << "[Scenario 2: no timeout, succesful read]" << std::endl;
    // Create and connect I/O objects.
    tcp::socket server_socket(io_service);
    tcp::socket client_socket(io_service);
    connect_sockets(acceptor, server_socket, client_socket);

    // Start read with timeout on client.
    client client(client_socket);
    client.async_read_with_timeout(boost::posix_time::seconds(10));

    // Allow do_read_with_timeout to intiate actual operations.
    io_service.run_one();

    // Write to client.
    boost::asio::write(server_socket, boost::asio::buffer("test"));

    // Run timeout and read operations.
    io_service.run();
    assert(client.status() == client::read_success);
  }

  // Scenario 3: no timeout, failed read
  // The server closes the connection before the timeout, causing the
  // async_read operation to fail and cancel the async_wait operation.
  {
    std::cout << "[Scenario 3: no timeout, failed read]" << std::endl;
    // Create and connect I/O objects.
    tcp::socket server_socket(io_service);
    tcp::socket client_socket(io_service);
    connect_sockets(acceptor, server_socket, client_socket);

    // Start read with timeout on client.
    client client(client_socket);
    client.async_read_with_timeout(boost::posix_time::seconds(10));

    // Allow do_read_with_timeout to intiate actual operations.
    io_service.run_one();

    // Close the socket.
    server_socket.close();

    // Run timeout and read operations.
    io_service.run();
    assert(client.status() == client::read_failure);
  }

  // Scenario 4: timeout and read success
  // The server writes data, but the io_service is not ran until the
  // timer has had time to expire.  In this case, both the await_wait and
  // asnyc_read operations complete, but the order in which the
  // handlers run is indeterminiate.
  {
    std::cout << "[Scenario 4: timeout and read success]" << std::endl;
    // Create and connect I/O objects.
    tcp::socket server_socket(io_service);
    tcp::socket client_socket(io_service);
    connect_sockets(acceptor, server_socket, client_socket);

    // Start read with timeout on client.
    client client(client_socket);
    client.async_read_with_timeout(boost::posix_time::seconds(0));

    // Allow do_read_with_timeout to intiate actual operations.
    io_service.run_one();

    // Allow the timeout to expire, the write to the client, causing both
    // operations to complete with success.
    boost::this_thread::sleep_for(boost::chrono::seconds(1));
    boost::asio::write(server_socket, boost::asio::buffer("test"));

    // Run timeout and read operations.
    io_service.run();
    assert(   (client.status() == client::timeout)
           || (client.status() == client::read_success));
  }
}
Run Code Online (Sandbox Code Playgroud)

及其输出:

[Scenario 1: timeout]
timeout occured
read failed: Operation canceled
[Scenario 2: no timeout, succesful read]
read was succesful
timeout cancelled
[Scenario 3: no timeout, failed read]
read failed: End of file
timeout cancelled
[Scenario 4: timeout and read success]
read was succesful
timeout occured, but handle_read ran first
Run Code Online (Sandbox Code Playgroud)