Form Sync to Async to As-Sync

The objective is to implement a simple TCP Echo Server that accepts TCP incoming connections and simply echoes anything that is sent to it.

We start off with a very simple, synchronous implementation and use that to motivate the use of synchronous operations, which is what ASIO is all about.

Finally, we escape callback hell by introducing C++20 Coroutines.

Synchronous

echo_sync.cpp

Let’s start off simple: Introduce the io_context and acceptor classes and how to create a simple TCP listening socket accepting incoming connections.

#include <boost/asio.hpp>

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

void session(tcp::socket socket)
{
   std::array<char, 64 * 1024> data;
   for (;;)
   {
      boost::system::error_code ec;
      std::size_t n = socket.read_some(buffer(data), ec);
      if (ec == error::eof)
         return;
      write(socket, buffer(data, n));
   }
}

void server(tcp::acceptor acceptor)
{
   for (;;)
      session(acceptor.accept());
}

int main()
{
   io_context context;
   server(tcp::acceptor{context, {tcp::v6(), 55555}});
}

What’s wrong here?

Warning

This code can handle only one connection at a time!

Synchronous (with threads)

To solve the problem, we can simply start a new thread for each accepted connection:

void server(tcp::acceptor acceptor)
{
   for (;;)
      std::thread(session, acceptor.accept()).detach();  // was: session(a.accept());
}

Now the echo server is capable of handling multiple connections in parallel. A new thread is created for each connection.

Warning

Spawning a thread per connection is slow and does not scale!

Asynchronous

So, spawning a new thread for each connection does not scale. So what can we do instead? Blocking a thread to wait for something to happen is not an option any more.

Enter the world of Asynchronous Operations. Instead of using their sync counterparts, we now have async_accept. In the echo loop, it is async_read_some and async_write_some.

echo_async

main()

int main()
{
   boost::asio::io_context context;
   server server(tcp::acceptor{context, {tcp::v6(), 55555}});
   context.run();
}

Server

class server
{
public:
   explicit server(tcp::acceptor acceptor)
      : acceptor_(std::move(acceptor))
   {
      do_accept();
   }

private:
   void do_accept()
   {
      acceptor_.async_accept([this](error_code ec, tcp::socket socket)
         {
            if (!ec)
               std::make_shared<session>(std::move(socket))->start();

            do_accept();
         });
   }

   tcp::acceptor acceptor_;
};

Session

class session : public std::enable_shared_from_this<session>
{
public:
   explicit session(tcp::socket socket) : socket_(std::move(socket)) {}
   void start() { do_read(); }

private:
   void do_read()
   {
      auto self(shared_from_this());
      socket_.async_read_some(buffer(data_),
                              [this, self](error_code ec, std::size_t length)
                              {
                                 if (!ec)
                                    do_write(length);
                              });
   }

   void do_write(std::size_t length)
   {
      auto self(shared_from_this());
      boost::asio::async_write(socket_, buffer(data_, length),
                              [this, self](error_code ec, std::size_t /* length */)
                              {
                                 if (!ec)
                                    do_read();
                              });
   }

   tcp::socket socket_;
   std::array<uint8_t, 64 * 1024> data_;
};

The control flow can be visualized like this:

_images/async.png

Also note that the session state is maintained as class members (socket_ and data_).

As-Synchronous (Coroutines)

Modern implementations using C++20 coroutines (co_await / awaitable) with Asio’s awaitable primitives. Key points:

  • Offers sequential-style code while remaining non-blocking.

  • Removes much of the boilerplate and state-machine complexity of callbacks.

  • Can be run single-threaded or on a multi-threaded io_context.

  • Typically uses asio::awaitable and use_awaitable completion token (or similar) provided by the Asio variant used in this project.

#include <boost/asio.hpp>

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

awaitable<void> session(tcp::socket socket)
{
   std::array<char, 64 * 1024> data;
   for (;;)
   {
      size_t n = co_await socket.async_read_some(buffer(data));
      co_await async_write(socket, buffer(data, n));
   }
}

awaitable<void> server(tcp::acceptor a)
{
   for (;;)
      co_spawn(a.get_executor(), session(co_await a.async_accept()), detached);
}

int main()
{
   io_context context;
   co_spawn(context, server({context, {tcp::v6(), 55555}}), detached);
   context.run();
}

Notes on choosing an implementation

  • Start with sync for learning and debugging.

  • Use thread-sync or thread pools for moderate concurrency with simpler synchronous code structure.

  • Prefer async or coro for high concurrency and lower resource usage.

  • Coroutines give clearer, maintainable code compared to callback-based async, at the cost of needing a modern C++ toolchain and coroutine-aware Asio setup.