【发布时间】:2014-05-22 20:28:13
【问题描述】:
我有一个使用 Boost.Asio 进行 TCP 和 UDP 套接字通信的应用程序。我知道“Asio”中的“A”代表 Asynchronous,因此该库倾向于鼓励您尽可能使用异步 I/O。我有一些情况下同步套接字读取更可取。但是,与此同时,我想对所述接收调用设置超时,因此不可能无限期地阻塞读取。
这在 Boost.Asio 用户中似乎是一个很常见的问题,过去有关该主题的 Stack Overflow 问题如下:
- C++ Boost ASIO: how to read/write with a timeout?
- asio::read with timeout
- boost asio timeout
- How to set a timeout on blocking sockets in boost asio?
可能还有更多。甚至还有examples in the documentation 用于如何实现带超时的同步操作。他们归结为将同步操作转换为异步操作,然后与 asio::deadline_timer 并行启动它。然后,计时器的到期处理程序可以在超时到期的情况下取消异步读取。这看起来像这样(sn-p 取自上面的链接示例):
std::size_t receive(const boost::asio::mutable_buffer& buffer,
boost::posix_time::time_duration timeout, boost::system::error_code& ec)
{
// Set a deadline for the asynchronous operation.
deadline_.expires_from_now(timeout);
// Set up the variables that receive the result of the asynchronous
// operation. The error code is set to would_block to signal that the
// operation is incomplete. Asio guarantees that its asynchronous
// operations will never fail with would_block, so any other value in
// ec indicates completion.
ec = boost::asio::error::would_block;
std::size_t length = 0;
// Start the asynchronous operation itself. The handle_receive function
// used as a callback will update the ec and length variables.
socket_.async_receive(boost::asio::buffer(buffer),
boost::bind(&client::handle_receive, _1, _2, &ec, &length));
// Block until the asynchronous operation has completed.
do io_service_.run_one(); while (ec == boost::asio::error::would_block);
return length;
}
这实际上是一个相对干净的解决方案:启动异步操作,然后手动轮询asio::io_service 以一次执行一个异步处理程序,直到async_receive() 完成或计时器到期。
但是,如果套接字的底层 I/O 服务已经在一个或多个后台线程中运行呢? 在这种情况下,不能保证异步操作的处理程序会由上述 sn-p 中的前台线程运行,因此 run_one() 直到稍后(可能不相关)处理程序执行后才会返回。这会使套接字读取相当迟钝。
asio::io_service 有一个poll_one() 函数,它将检查服务的队列而不阻塞,但我看不到阻塞前台线程(模拟同步调用行为)直到处理程序执行的好方法,除了没有后台线程正在执行asio::io_service::run() 的情况。
我看到了两种可能的解决方案,但我都不喜欢:
在启动异步操作后,使用条件变量或类似构造使前台线程阻塞。在
async_receive()调用的处理程序中,向条件变量发出信号以解除对线程的阻塞。这会为每次读取带来一些锁定,我想避免这种情况,因为我想在 UDP 套接字读取上实现最大可能的吞吐量。否则,它是可行的,除非有更好的方法出现,否则我可能会这样做。确保套接字有自己的
asio::io_service,它没有被任何后台线程运行。这使得在需要的情况下使用带有套接字的异步 I/O 变得更加困难。
有什么其他方法可以安全地完成此任务吗?
旁白: 之前的一些 SO 问题的答案主张使用 SO_RCVTIMEO 套接字选项来实现套接字读取超时。这在理论上听起来不错,但至少在我的平台上似乎不起作用(Ubuntu 12.04,Boost v1.55)。我可以设置套接字超时,但它不会给 Asio 带来预期的效果。相关代码在/boost/asio/detail/impl/socket_ops.ipp:
size_t sync_recvfrom(socket_type s, state_type state, buf* bufs,
size_t count, int flags, socket_addr_type* addr,
std::size_t* addrlen, boost::system::error_code& ec)
{
if (s == invalid_socket)
{
ec = boost::asio::error::bad_descriptor;
return 0;
}
// Read some data.
for (;;)
{
// Try to complete the operation without blocking.
signed_size_type bytes = socket_ops::recvfrom(
s, bufs, count, flags, addr, addrlen, ec);
// Check if operation succeeded.
if (bytes >= 0)
return bytes;
// Operation failed.
if ((state & user_set_non_blocking)
|| (ec != boost::asio::error::would_block
&& ec != boost::asio::error::try_again))
return 0;
// Wait for socket to become ready.
if (socket_ops::poll_read(s, 0, ec) < 0)
return 0;
}
}
如果套接字读取超时,上面对recvfrom() 的调用将返回EAGAIN 或EWOULDBLOCK,它们将被转换为boost::asio::error::try_again 或boost::asio::error::would_block。在这种情况下,上面的代码将调用poll_read() 函数,在我的平台上看起来像:
int poll_read(socket_type s, state_type state, boost::system::error_code& ec)
{
if (s == invalid_socket)
{
ec = boost::asio::error::bad_descriptor;
return socket_error_retval;
}
pollfd fds;
fds.fd = s;
fds.events = POLLIN;
fds.revents = 0;
int timeout = (state & user_set_non_blocking) ? 0 : -1;
clear_last_error();
int result = error_wrapper(::poll(&fds, 1, timeout), ec);
if (result == 0)
ec = (state & user_set_non_blocking)
? boost::asio::error::would_block : boost::system::error_code();
else if (result > 0)
ec = boost::system::error_code();
return result;
}
我剪掉了有条件地为其他平台编译的代码。如您所见,如果套接字不是非阻塞套接字,它最终会以无限超时调用poll(),因此会阻塞直到套接字有要读取的数据(并在超时时阻止尝试)。因此,SO_RCVTIMEO 选项无效。
【问题讨论】:
标签: sockets timeout boost-asio synchronous