The Boost C++ Libraries

Coroutines

Since version 1.54.0, Boost.Asio supports coroutines. While you could use Boost.Coroutine directly, explicit support of coroutines in Boost.Asio makes it easier to use them.

Coroutines let you create a structure that mirrors the actual program logic. Asynchronous operations don’t split functions, because there are no handlers to define what should happen when an asynchronous operation completes. Instead of having handlers call each other, the program can use a sequential structure.

Example 32.7. Coroutines with Boost.Asio
#include <boost/asio/io_service.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/write.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <list>
#include <string>
#include <ctime>

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

io_service ioservice;
tcp::endpoint tcp_endpoint{tcp::v4(), 2014};
tcp::acceptor tcp_acceptor{ioservice, tcp_endpoint};
std::list<tcp::socket> tcp_sockets;

void do_write(tcp::socket &tcp_socket, yield_context yield)
{
  std::time_t now = std::time(nullptr);
  std::string data = std::ctime(&now);
  async_write(tcp_socket, buffer(data), yield);
  tcp_socket.shutdown(tcp::socket::shutdown_send);
}

void do_accept(yield_context yield)
{
  for (int i = 0; i < 2; ++i)
  {
    tcp_sockets.emplace_back(ioservice);
    tcp_acceptor.async_accept(tcp_sockets.back(), yield);
    spawn(ioservice, [](yield_context yield)
      { do_write(tcp_sockets.back(), yield); });
  }
}

int main()
{
  tcp_acceptor.listen();
  spawn(ioservice, do_accept);
  ioservice.run();
}

The function to call to use coroutines with Boost.Asio is boost::asio::spawn(). The first parameter passed must be an I/O service object. The second parameter is the function that will be the coroutine. This function must accept as its only parameter an object of type boost::asio::yield_context. It must have no return value. Example 32.7 uses do_accept() and do_write() as coroutines. If the function signature is different, as is the case for do_write(), you must use an adapter like std::bind or a lambda function.

Instead of a handler, you can pass an object of type boost::asio::yield_context to asynchronous functions. do_accept() passes the parameter yield to async_accept(). In do_write(), yield is passed to async_write(). These function calls still start asynchronous operations, but no handlers will be called when the operations complete. Instead, the context in which the asynchronous operations were started is restored. When these asynchronous operations complete, the program continues where it left off.

do_accept() contains a for loop. A new socket is passed to async_accept() every time the function is called. Once a client establishes a connection, do_write() is called as a coroutine with boost::asio::spawn() to send the current time to the client.

The for loop makes it easy to see that the program can serve two clients before it exits. Because the example is based on coroutines, the repeated execution of an asynchronous operation can be implemented in a for loop. This improves the readability of the program since you don’t have to trace potential calls to handlers to find out when the last asynchronous operation will be completed. If the time server needs to support more than two clients, only the for loop has to be adapted.

Exercise

Develop a client and a server which can transfer a file from one computer to another. When the server is started, it should display a list of IP addresses of all local interfaces and wait for the client to connect. When the client is started, an IP address from the server and the name of a local file should be passed as command line options. The client should transfer the file to the server which saves it to the current working directory. During transmission the client should display some sort of progress indicator so that the user knows that the transmission is ongoing. Implement the client and server with coroutines.