3

Very short summarized:

I am implementing a simple TCP server using boost::asio that allows for non-blocking acception of new connnections. Inside the logic of handling the new connection a lot of work will be performed that might take up to several minutes. In particular I am going to start a new process and wait for it to finish while reading stdin, stderr and it's return code. Just image I want to start g++ for every connection, compile several source files and get the build information. While g++ runs in the seperate process I still want to be able to accept new connections.

I am a newcomer to boost::asio and looking for some design advice and input to my current ideas.

Option #1: Using a thread for each connection and detach it

#include <boost/asio.hpp>

#include "tcp_connection.hpp"
#include <thread>
#include <functional>

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

class tcp_server
{
public:
  tcp_server(boost::asio::io_context& io_context, unsigned short port_num)
   : m_io_context(io_context),
     m_acceptor(io_context, tcp::endpoint(tcp::v4(), port_num)),
     m_port_num(port_num)
  {
      // create initial connection that will be accepted
      create_connection();
  }

private:
    void create_connection()
    {
        // create new connection that will be accepted
        tcp_connection::pointer new_connection = tcp_connection::create(m_io_context);
        // can't mix std::bind with boost::asio::placeholders ...
        m_acceptor.async_accept(new_connection->socket(),
                                boost::bind(&tcp_server::handle_accept, this,
                                          boost::asio::placeholders::error));

        // save new connection to be handled next
        m_curr_connection = new_connection;
    }


    void handle_accept(const boost::system::error_code& error)
    {
        if(!error)
        {
            // run new connection in own thread 
            std::thread t(std::bind(&tcp_connection::run, m_curr_connection));
            // create next connection that will be accepted
            create_connection();
            // detach thread before it goes out of scope 
            t.detach();
        }
    }

    boost::asio::io_context& m_io_context;
    tcp::acceptor m_acceptor;
    tcp_connection::pointer m_curr_connection;
    unsigned short m_port_num;
};

So accepting of the connection is all performed asynchronously in the main-thread using async_accept. For the handling I am creating a worker thread that calls g++ and waits for it to finish. Yet the server can accept new connections in the meanwhile and start new compilations.

The run method of the connection looks similar to this withour error handling

auto prog = boost::process::search_path("g++");
boost::asio::io_context io_context;

std::future<std::string> data;
boost::process::child processCobol(prog, "main.cpp"
                                   boost::process::std_in.close(),
                                   boost::process::std_out > boost::process::null,
                                   boost::process::std_err > data,
                                   io_context);

io_context.run();
m_message = data.get();

I am using asynchronous I/O here as well, however, it would also be suffiecent to read the result back synchronously for me at the moment.

Option #2: Using a fork approach

Assume I have a libg++ that I can link straight to the server. I could use a "classic" fork approach for each connection such as shown in here: https://www.boost.org/doc/libs/1_52_0/doc/html/boost_asio/example/fork/process_per_connection.cpp and put a call to g++_compile() straight after the fork. Still I can accept new connection as I have a separate process for each connection.

Option #3: Use boost::process::spawn and read back outcome via shared memory, e.g using boost::interprocess

Instead of creating a child process I could spawn a new process, detach it and at some point read the outcome back via shared memory. Something tells me that this is not a really good idea D:

Option #4: ?

Is there actually any way to do this without the need of using a helper thread or process for each connection? In the end at some point I need to wait for the result in my main-thread, blocking the acception of new connections. I got my hands on a coroutine a bit, but still lack to understand its details and don't think it will help me either in this scenario.

Thank you very much!

Paxi1337
  • 85
  • 8

2 Answers2

2

Have you tried this solution?

1) Create an asio::thread_group and pass a thread-function F which calls io_service::run.

asio::thread_group tg{ [](){io_service.run()}, x }; \\ x is the number of threads you want

2) In your handle_accept, rather than doing std::thread t(std::bind(&tcp_connection::run, m_curr_connection)); and t.detach()

do post(io_service, your_work) where your_work can be any callback or functionObject.

The free standing asio::post will put your_work in io_service::queue (briefly), from where it may get concurrently executed.

On a note: if your_work may block in the execution, then you can futher turn them into asynchronous rather than concurrently-blocking' (as other pending handlers may starve)

RaGa__M
  • 2,550
  • 1
  • 23
  • 44
  • Thanks for your answer and sorry for the late reply. I did rearange some things myself in the meanwhile and was busy progressing on the project itself. I will probably try this out someday but not now. Will keep you updated in case and got you an upvote anyways for now ;) – Paxi1337 Jul 03 '19 at 09:00
0

I did implement #2 ahead of the mentioned example now as well and it seems to work as intended for my needs. Still I would be very interested in some input, especially considering #4.

Paxi1337
  • 85
  • 8