The Boost C++ Libraries

I/O Services and I/O Objects

Programs that use Boost.Asio for asynchronous data processing are based on I/O services and I/O objects. I/O services abstract the operating system interfaces that process data asynchronously. I/O objects initiate asynchronous operations. These two concepts are required to separate tasks cleanly: I/O services look towards the operating system API, and I/O objects look towards tasks developers need to do.

As a user of Boost.Asio you normally don’t connect with I/O services directly. I/O services are managed by an I/O service object. An I/O service object is like a registry where I/O services are listed. Every I/O object knows its I/O service and gets access to its I/O service through the I/O service object.

Boost.Asio defines boost::asio::io_service, a single class for an I/O service object. Every program based on Boost.Asio uses an object of type boost::asio::io_service. This can also be a global variable.

While there is only one class for an I/O service object, several classes for I/O objects exist. Because I/O objects are task oriented, the class that needs to be instantiated depends on the task. For example, if data has to be sent or received over a TCP/IP connection, an I/O object of type boost::asio::ip::tcp::socket can be used. If data has to be transmitted asynchronously over a serial port, boost::asio::serial_port can be instantiated. If you want to wait for a time period to expire, you can use the I/O object boost::asio::steady_timer.

boost::asio::steady_timer is like an alarm clock. Instead of waiting for a blocking function to return when the alarm clock rings, your program will be notified. Because boost::asio::steady_timer just waits for a time period to expire, it would seem as though no external resource is accessed. However, in this case the external resource is the capability of the operating system to notify a program when a time period expires. This frees a program from creating a new thread just to call a blocking function. Since boost::asio::steady_timer is a very simple I/O object, it will be used to introduce Boost.Asio.

Note

Because of a bug in Boost.Asio, it is not possible to compile some of the following examples with Clang. The bug has been reported in ticket 8835. As a workaround, if you replace the types from std::chrono with the respective types from boost::chrono, you can compile the examples with Clang.

Example 32.1. Using boost::asio::steady_timer
#include <boost/asio/io_service.hpp>
#include <boost/asio/steady_timer.hpp>
#include <chrono>
#include <iostream>

using namespace boost::asio;

int main()
{
  io_service ioservice;

  steady_timer timer{ioservice, std::chrono::seconds{3}};
  timer.async_wait([](const boost::system::error_code &ec)
    { std::cout << "3 sec\n"; });

  ioservice.run();
}

Example 32.1 creates an I/O service object, ioservice, and uses it to initialize the I/O object timer. Like boost::asio::steady_timer, all I/O objects expect an I/O service object as a first parameter in their constructor. Since timer represents an alarm clock, a second parameter can be passed to the constructor of boost::asio::steady_timer that defines the specific time or time period when the alarm clock should ring. In Example 32.1, the alarm clock is set to ring after 3 seconds. The time starts with the definition of timer.

Instead of calling a blocking function that will return when the alarm clock rings, Boost.Asio lets you start an asynchronous operation. To do this, call the member function async_wait(), which expects a handler as the sole parameter. A handler is a function or function object that is called when the asynchronous operation ends. In Example 32.1, a lambda function is passed as a handler.

async_wait() returns immediately. Instead of waiting three seconds until the alarm clock rings, the lambda function is called after three seconds. When async_wait() returns, a program can do something else.

A member function like async_wait() is called non-blocking. I/O objects usually also provide blocking member functions as alternatives. For example, you can call the blocking member function wait() on boost::asio::steady_timer. Because this member function is blocking, no handler is passed. wait() returns at a specific time or after a time period.

The last statement in main() in Example 32.1 is a call to run() on the I/O service object. This call is required because operating system-specific functions have to take over control. Remember that it is the I/O service in the I/O service object which implements asynchronous operations based on operating system-specific functions.

While async_wait() initiates an asynchronous operation and returns immediately, run() blocks. Many operating systems support asynchronous operations only through a blocking function. The following example shows why this usually isn’t a problem.

Example 32.2. Two asynchronous operations with boost::asio::steady_timer
#include <boost/asio/io_service.hpp>
#include <boost/asio/steady_timer.hpp>
#include <chrono>
#include <iostream>

using namespace boost::asio;

int main()
{
  io_service ioservice;

  steady_timer timer1{ioservice, std::chrono::seconds{3}};
  timer1.async_wait([](const boost::system::error_code &ec)
    { std::cout << "3 sec\n"; });

  steady_timer timer2{ioservice, std::chrono::seconds{4}};
  timer2.async_wait([](const boost::system::error_code &ec)
    { std::cout << "4 sec\n"; });

  ioservice.run();
}

In Example 32.2, two objects of type boost::asio::steady_timer are used. The first I/O object is an alarm clock that rings after three seconds. The other is an alarm clock ringing after four seconds. After both time periods expire, the lambda functions that were passed to async_wait() will be called.

run() is called on the only I/O service object in this example. This call passes control to the operating system functions that execute asynchronous operations. With their help, the first lambda function is called after three seconds and the second lambda function after four seconds.

It might come as a surprise that asynchronous operations require a call to a blocking function. However, this is not a problem because the program has to be prevented from exiting. If run() wouldn’t block, main() would return, and the program would exit. If you don’t want to wait for run() to return, you only need to call run() in a new thread.

The reason why the examples above exit after a while is that run() returns if there are no pending asynchronous operations. Once all alarm clocks have rung, no asynchronous operations exist that the program needs to wait for.