6

This is somewhat similar to boost::asio async performance. As there is no conclusive answer to that question, I'm posting a similar question with sample code and stats to demonstrate the problem.

Below, I've sample synchronous and asynchronous server applications, which send 25 byte message to the client in a loop continuously. On the client side, I'm checking at what rate it is able to receive the messages. The sample setup is pretty simple. In synchronous server case, it spawns a new thread per client connection and the thread keeps sending the 25-byte message in a loop. In asynchronous server case as well it spawns a new thread per client connection and the thread keeps sending the 25-byte message in a loop, using asynchronous write (main thread is the one which calls ioservice.run()). For the performance testing I'm using only one client.

Synchronous server code:

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

using boost::asio::ip::tcp;

class tcp_connection : public boost::enable_shared_from_this<tcp_connection>
{
public:
    typedef boost::shared_ptr<tcp_connection> pointer;

    static pointer create(boost::asio::io_service& io_service)
    {
       return pointer(new tcp_connection(io_service));
    }

    tcp::socket& socket()
    {
        return socket_;
    }

    void start()
    {
        for (;;) {
            try {
                ssize_t len = boost::asio::write(socket_, boost::asio::buffer(message_));
                if (len != message_.length()) {
                    std::cerr<<"Unable to write all the bytes"<<std::endl;
                    break;
                }
                if (len == -1) {
                    std::cerr<<"Remote end closed the connection"<<std::endl;
                    break;
                }
            }
            catch (std::exception& e) {
                std::cerr<<"Error while sending data"<<std::endl;
                break;
            }
        }
    }

private:
    tcp_connection(boost::asio::io_service& io_service)
        : socket_(io_service),
          message_(25, 'A')
    {
    }

    tcp::socket socket_;
    std::string message_;
};

class tcp_server
{
public:
    tcp_server(boost::asio::io_service& io_service)
        : acceptor_(io_service, tcp::endpoint(tcp::v4(), 1234))
    {
        start_accept();
    }

private:
    void start_accept()
    {
        for (;;) {
            tcp_connection::pointer new_connection =
                tcp_connection::create(acceptor_.get_io_service());
            acceptor_.accept(new_connection->socket());
            boost::thread(boost::bind(&tcp_connection::start, new_connection));
        }
    }
    tcp::acceptor acceptor_;
};

int main()
{
    try {
        boost::asio::io_service io_service;
        tcp_server server(io_service);
    }
    catch (std::exception& e) {
        std::cerr << e.what() << std::endl;
    }
    return 0;
}

ASynchronous server code:

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

#include <boost/thread.hpp>

using boost::asio::ip::tcp;

class tcp_connection
        : public boost::enable_shared_from_this<tcp_connection>
{
public:
    typedef boost::shared_ptr<tcp_connection> pointer;

    static pointer create(boost::asio::io_service& io_service)
    {
        return pointer(new tcp_connection(io_service));
    }

    tcp::socket& socket()
    {
        return socket_;
    }

    void start()
    {
        while (socket_.is_open()) {
            boost::asio::async_write(socket_, boost::asio::buffer(message_),
                boost::bind(&tcp_connection::handle_write, shared_from_this(),
                            boost::asio::placeholders::error,
                            boost::asio::placeholders::bytes_transferred));
        }
    }

private:
    tcp_connection(boost::asio::io_service& io_service)
        : socket_(io_service),
          message_(25, 'A')
    {
    }

    void handle_write(const boost::system::error_code& error,
                      size_t bytes_transferred)
    {
        if (error) {
            if (socket_.is_open()) {
                std::cout<<"Error while sending data asynchronously"<<std::endl;
                socket_.close();
            }
        }
    }

    tcp::socket socket_;
    std::string message_;
};

class tcp_server
{
public:
    tcp_server(boost::asio::io_service& io_service)
        : acceptor_(io_service, tcp::endpoint(tcp::v4(), 1234))
    {
        start_accept();
    }

private:
    void start_accept()
    {
        tcp_connection::pointer new_connection =
                tcp_connection::create(acceptor_.get_io_service());
        acceptor_.async_accept(new_connection->socket(),
                boost::bind(&tcp_server::handle_accept, this, new_connection,
                        boost::asio::placeholders::error));
    }

    void handle_accept(tcp_connection::pointer new_connection,
                       const boost::system::error_code& error)
    {
        if (!error) {
            boost::thread(boost::bind(&tcp_connection::start, new_connection));
        }

        start_accept();
    }

    tcp::acceptor acceptor_;
};

int main()
{
    try {
        boost::asio::io_service io_service;
        tcp_server server(io_service);
        io_service.run();
    }
    catch (std::exception& e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

Client code:

#include <iostream>

#include <boost/asio.hpp>
#include <boost/array.hpp>

int main(int argc, char* argv[])
{
    if (argc != 3) {
        std::cerr<<"Usage: client <server-host> <server-port>"<<std::endl;
        return 1;
    }

    boost::asio::io_service io_service;
    boost::asio::ip::tcp::resolver resolver(io_service);
    boost::asio::ip::tcp::resolver::query query(argv[1], argv[2]);
    boost::asio::ip::tcp::resolver::iterator it = resolver.resolve(query);
    boost::asio::ip::tcp::resolver::iterator end;
    boost::asio::ip::tcp::socket socket(io_service);
    boost::asio::connect(socket, it);

//    Statscollector to periodically print received messages stats
//    sample::myboost::StatsCollector stats_collector(5);
//    sample::myboost::StatsCollectorScheduler statsScheduler(stats_collector);
//    statsScheduler.start();

    for (;;) {
        boost::array<char, 25> buf;
        boost::system::error_code error;
        size_t len = socket.read_some(boost::asio::buffer(buf), error);
//        size_t len = boost::asio::read(socket, boost::asio::buffer(buf));
        if (len != buf.size()) {
            std::cerr<<"Length is not "<< buf.size() << " but "<<len<<std::endl;
        }
//        stats_collector.incr_msgs_received();
    }
}

Question:

When the client is running against synchronous server it is able to receive around 700K msgs/sec but when it is running against asynchronous server the performance is dropped to around 100K-120K msgs/sec. I know that one should use asynchronous IO for scalability when we have more number of clients and in the above case as I'm using only a single client, the obvious advantage of asynchronous IO is not evident. But the question is, is asynchronous IO expected to effect the performance so badly for a single client case or am I missing some obvious best practices to follow with asynchronous IO? Is the significant drop in the performance is because of the thread switch between ioservice thread (which is main thread in the above case) and connection thread?

Setup: I'm using BOOST 1.47 on Linux machine.

Community
  • 1
  • 1
Donald Alan
  • 61
  • 1
  • 2
  • While I am just getting start on ASIO myself, I think you are supposed to execute `ioservice.run()` from every thread that you want to work in parallel. See the [tutorial](http://www.boost.org/doc/libs/1_55_0/doc/html/boost_asio/tutorial/tuttimer5.html) – ted Mar 21 '14 at 15:21
  • @ted in the case of synchronous IO it is not required to call ioservice.run() explicitly. See the example [link](http://www.boost.org/doc/libs/1_55_0/doc/html/boost_asio/example/cpp03/echo/blocking_tcp_echo_server.cpp) – Donald Alan Mar 21 '14 at 15:43

1 Answers1

2

This is not how the asynchronous send is supposed to be used: in this way the connection's thread is putting in the asio queue more and more write requests, and concurrently the thread invoking ioservice.run() is dequeuing them.

The low performance is very probably due to the fact that there is high contention on the ioservice work queue on part of the main thread (producer) and the thread running the ioservice (consumer).

Furthermore if you monitor your memory you should see it growing, at the point to block your system eventually: I do expect the producer being faster than the consumer.

The correct approach (untested), reporting only the two relevant methods, should be something like:

    void start()
    {
        boost::asio::async_write(socket_, boost::asio::buffer(message_),
            boost::bind(&tcp_connection::handle_write, shared_from_this(),
                        boost::asio::placeholders::error,
                        boost::asio::placeholders::bytes_transferred));
    }

    void handle_write(const boost::system::error_code& error,
                      size_t bytes_transferred)
    {
        if (error) {
            if (socket_.is_open()) {
                std::cout<<"Error while sending data asynchronously"<<std::endl;
                socket_.close();
            }
        }
        if (socket_.is_open()) {
            boost::asio::async_write(socket_, boost::asio::buffer(message_),
                boost::bind(&tcp_connection::handle_write, shared_from_this(),
                            boost::asio::placeholders::error,
                            boost::asio::placeholders::bytes_transferred));
        }

    }    

That is, the connection thread only "ignite" an event driven loop, that is run op part of the ioservice's thread: as soon as a write is performed, the callback is invoked in order to post the next one.

Sigi
  • 4,826
  • 1
  • 19
  • 23
  • Yes, as mentioned in my original post, I'm also suspecting that the degrade in the performance is due to thread switch/contention between ioservice thread and connection thread. Regarding the ioservice thread itself calling asyn_write....I think though in this particular sample code it looks fine, but in typical use case it would always be non-ioservice thread which would be calling async_write. – Donald Alan Mar 24 '14 at 13:57
  • So, the more number of messages we have to send with a single client it seems like asynchronous IO adds more overhead/performance problems when compared to synchronous IO. Of course, this would be compensated when more and more clients gets added because we don't need to create one thread per client in asynchronous IO case. – Donald Alan Mar 24 '14 at 14:06
  • I'm not sure you have got the meaning of my answer. Does my solution solve the asynchronous low-performance problem? The overhead is minimal, the flexibility is very higher. Only problem, it's slightly more cumbersome to write. If not clear the difference, I can add some nice diagrams to the answer... – Sigi Mar 24 '14 at 14:14
  • I think I got what you are saying. The connection thread trigger a call to ioservice and ioservice thread is the one which continuously tries to send the messages (let me know if I'm missing something). This works in this sample case because _message is not changing. But in a typical case, the message would be generated in some application thread and it needs to be sent to client. So, generally ioservice thread won't call async_write. – Donald Alan Mar 24 '14 at 15:01
  • 2nd part not true: in real applications the message would be generated by the ioservice.run() threads - that become the thread pool of the application. This is really how async asio applications are supposed to be designed. The idea is *event driven* - the even here is "write performed" and the action is "evaluate what to send next" (handle_write())... and so on - that's the *event driven loop* I was mentioning. – Sigi Mar 24 '14 at 16:29
  • but there is nothing in boost asio just to notify that write is performed like similar to [Java NIO](http://docs.oracle.com/javase/7/docs/api/java/nio/channels/SelectionKey.html#interestOps(int)) . Here the only way to trigger the event handler is by actually trying to send some data on the socket asynchronously (so both ioservice thread and application thread would do async_write, right? Is this desirable). – Donald Alan Mar 24 '14 at 16:59
  • @Donald Alan: handle_write is called when write has been done, so looping on sending message_ May be something is missing in the code of Sigismondo, like waiting on message_ filled up by a new value, or if message_ is a FIFO queue waiting while message_ is empty. You should have a look to that ::http://stackoverflow.com/questions/5282659/some-clarification-needed-about-synchronous-versus-asynchronous-asio-operations/5291002#5291002 IMHO, I do not see any reason for significant differences in performance. – Jean Davy Mar 24 '14 at 18:43
  • 1
    @DonaldAlan not only:: you can use timed events as http://www.boost.org/doc/libs/1_55_0/doc/html/boost_asio/tutorial/tuttimer3.html. Again the *event driven* equivalent of performing a loop with a sleep, is to let the timer callback ignite itself again and again. The huge difference with respect to the classical approach is that using an event driven loop, the threads in the pool are free to execute other callbacks. With the "classical approach", the thread is stuck in the sleep. You can also "ignite immediately": http://www.boost.org/doc/libs/1_55_0/doc/html/boost_asio/reference/spawn.html – Sigi Mar 24 '14 at 20:19
  • @DonaldAlan you forgot to say if the answer resolves your problem, and if so, accept the answer. – Pavel P Jul 31 '17 at 16:20