StrikeTeam StrikeTeam - 6 months ago 33
Linux Question

Is async_connect really asynchronous under GNU/Linux?

I wanted to check whether

Boost Asio
really performs an asynchronous connect or not. According to the diagrams corresponding to the asynchronous calls published in
Asio
's basics, the operation is started when the
io_service
signals the operating system, and therefore I understand that right after executing the
async.connect
instruction the system attempts to perform that connection.

What I have understood is, if you don't call
run
you just miss the results, but the operations are —probably— completed. So I tried that creating a dummy server with
nc -l -p 9000
and then using the code attached below.

With a debugger, I have gone statement by statement and I have stopped right before the
run
call to the
io_service
. In the server, nothing happens. Nothing about the connect —this is obvious as the dummy server does not tell you about it— nor the
async_write
.

Immediately after calling the
run
function the corresponding messages pop up on the server side. I have been asking on
Boost
's IRC channel and after showing my
strace
, a really clever person told me that it probably is because the socket is not ready until
run
is called. Apparently, this does not happen on Windows.

Does that this mean that asynchronous is not really asynchronous under GNU/Linux operating systems? Does this mean that the diagram showed in the website does not correspond to GNU/Linux environments?

NOTE about "is not really asynchronous": Yes, it does not block the call and therefore the thread keeps running and doing things, but I mean asynchronous by starting the operations right after they have been executed.

Thank you very much indeed in advance.

The code

#include <iostream>
#include <string.h>
#include <boost/asio.hpp>
#include <boost/bind.hpp>

void connect_handler(const boost::system::error_code& error)
{
if(error)
{
std::cout << error.message() << std::endl;
exit(EXIT_FAILURE);
}
else
{
std::cout << "Successfully connected!" << std::endl;
}
}

void write_handler(const boost::system::error_code& error)
{
if(error)
{
std::cout << error.message() << std::endl;
exit(EXIT_FAILURE);
}
else
{
std::cout << "Yes, we wrote!" << std::endl;
}
}

int main()
{
boost::asio::io_service io_service;
boost::asio::ip::tcp::socket socket(io_service);
boost::asio::ip::tcp::endpoint endpoint(
boost::asio::ip::address::from_string("127.0.0.1"), 9000);

socket.async_connect(endpoint, connect_handler);

std::string hello_world("Hello World!");
boost::asio::async_write(socket, boost::asio::buffer(hello_world.c_str(),
hello_world.size()), boost::bind(write_handler,
boost::asio::placeholders::error));

io_service.run();
exit(EXIT_SUCCESS);
}


My strace

futex(0x7f44cd0ca03c, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x7f44cd0ca048, FUTEX_WAKE_PRIVATE, 2147483647) = 0
eventfd2(0, O_NONBLOCK|O_CLOEXEC) = 3
epoll_create1(EPOLL_CLOEXEC) = 4
timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC) = 5
epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLERR|EPOLLET, {u32=37842600, u64=37842600}}) = 0
write(3, "\1\0\0\0\0\0\0\0", 8) = 8
epoll_ctl(4, EPOLL_CTL_ADD, 5, {EPOLLIN|EPOLLERR, {u32=37842612, u64=37842612}}) = 0
socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 6
epoll_ctl(4, EPOLL_CTL_ADD, 6, {EPOLLIN|EPOLLPRI|EPOLLERR|EPOLLHUP|EPOLLET, {u32=37842704, u64=37842704}}) = 0
ioctl(6, FIONBIO, [1]) = 0
connect(6, {sa_family=AF_INET, sin_port=htons(9000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)
epoll_ctl(4, EPOLL_CTL_MOD, 6, {EPOLLIN|EPOLLPRI|EPOLLOUT|EPOLLERR|EPOLLHUP|EPOLLET, {u32=37842704, u64=37842704}}) = 0
epoll_wait(4, {{EPOLLIN, {u32=37842600, u64=37842600}}, {EPOLLOUT, {u32=37842704, u64=37842704}}}, 128, -1) = 2
poll([{fd=6, events=POLLOUT}], 1, 0) = 1 ([{fd=6, revents=POLLOUT}])
getsockopt(6, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
sendmsg(6, {msg_name(0)=NULL, msg_iov(1)=[{"Hello World!", 12}], msg_controllen=0, msg_flags=0}, MSG_NOSIGNAL) = 12
fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 5), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f44cd933000
write(1, "Successfully connected!\n", 24Successfully connected!
) = 24
epoll_wait(4, {}, 128, 0) = 0
write(1, "Yes, we wrote!\n", 15Yes, we wrote!
) = 15
exit_group(0) = ?
+++ exited with 0 +++

Answer

A weird thing about boost.asio -- not unique to it, but generally different from OS-specific asynchronous networking frameworks -- is that it does not rely on auxiliary threads. That has a number of important consequences, which can be boiled down to this: boost.asio is NOT for doing things in the background. Rather, it is for doing multiple things in the foreground.

io_service::run() is the "hub" of boost.asio, and a single-threaded program using boost.asio should expect to spend most of its time waiting inside io_service::run(), or executing a completion handler called by it. Depending on the OS-specific internal implementation, it might or might not be possible to have particular asynchronous operations running before that function gets called, which is why calling it is basically the first thing you do once you've kicked off your initial asynchronous requests.

Think of async_connect as "arming" your io_service with an asynchronous operation. The actual asynchrony happens during io_service::run(). In fact, calling async_connect followed immediately by async_write is a weird thing to do, and I'm a little surprised that it works. Ordinarily, you'd execute (or, rather, "arm") the async_write from within the connect_handler, because it's only at that point that you have a connected socket.