Die Boost C++ Bibliotheken

I/O Services und I/O Objekte

Programme, die Boost.Asio zur asynchronen Datenverarbeitung verwenden, basieren auf I/O Services und I/O Objekten. I/O Services abstrahieren Betriebssystemschnittstellen, die Daten asynchron verarbeiten können. I/O Objekte stellen Funktionen zur Verfügung, um asynchrone Operationen zu initiieren. Diese beiden Konzepte sind notwendig, um Aufgaben klar zu verteilen: I/O Services sind betriebssystemorientiert, I/O Objekte aufgabenorientiert.

Als Anwender von Boost.Asio kommen Sie mit I/O Services üblicherweise nicht direkt in Kontakt. I/O Services werden von einem I/O Serviceobjekt verwaltet. Sie können sich das I/O Serviceobjekt als Registrierungsdatenbank vorstellen, in der I/O Services automatisch registriert werden, wenn sie gebraucht werden. Jedes I/O Objekt kennt seinen I/O Service und erhält Zugriff auf diesen über das I/O Serviceobjekt. Sie stellen I/O Objekten lediglich ein I/O Serviceobjekt zur Verfügung – wann wie welche I/O Services registriert werden, geschieht automatisch.

Boost.Asio definiert mit boost::asio::io_service eine einzige Klasse für das I/O Serviceobjekt. Jedes Programm, das auf Boost.Asio basiert, verwendet ein Objekt vom Typ boost::asio::io_service. Das kann der Einfachheit halber global erstellt werden.

Während es lediglich eine Klasse für das I/O Serviceobjekt gibt, gibt es zahlreiche Klassen für I/O Objekte. Da I/O Objekte aufgabenorientiert sind, hängt es von den Aufgaben ab, die erledigt werden sollen, welche Klassen instanziiert werden müssen. Sollen zum Beispiel Daten über eine TCP/IP-Verbindung gesendet oder empfangen werden, kann ein I/O Objekt boost::asio::ip::tcp::socket verwendet werden. Sollen Daten asynchron über eine serielle Schnittstelle ausgetauscht werden, kann auf das I/O Objekt boost::asio::serial_port zugegriffen werden. Soll lediglich asynchron auf den Ablauf einer Zeitspanne gewartet werden, kann das I/O Objekt boost::asio::steady_timer verwendet werden.

boost::asio::steady_timer ist vergleichbar mit einem Wecker. Anstatt in einer blockierenden Funktion warten zu müssen, bis der Wecker klingelt, wird Ihr Programm nach Ablauf der Zeitspanne informiert. Da boost::asio::steady_timer lediglich auf den Ablauf einer Zeitspanne wartet, findet über dieses I/O Objekt scheinbar kein Zugriff auf eine externe Ressource statt. Die externe Ressource ist in diesem Fall die Fähigkeit des Betriebssystems, nach Ablauf der Zeitspanne dem Programm Bescheid zu geben, so dass im Programm zum Beispiel kein neuer Thread erstellt und in diesem mit einer blockierenden Funktion gewartet werden muss. Da es sich bei boost::asio::steady_timer um ein sehr einfaches I/O Objekt handelt, wird mit diesem in die Entwicklung auf Boost.Asio basierenden Programmen eingestiegen.

Anmerkung

Einige der folgenden Beispiele in diesem Kapitel können aufgrund eines Bugs in Boost.Asio 1.57.0 nicht mit Clang kompiliert werden. Der Bug ist im Ticket 8835 beschrieben. Wenn Sie die Typen aus std::chrono mit den entsprechenden Typen aus boost::chrono ersetzen, können Sie die Beispiele auch mit Clang übersetzen.

Beispiel 32.1. boost::asio::steady_timer in Aktion
#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();
}

Im Beispiel 32.1 wird in main() ein I/O Serviceobjekt ioservice erstellt, mit dem das I/O Objekt timer initialisiert wird. So wie boost::asio::steady_timer erwarten typischerweise alle I/O Objekte als ersten Parameter im Konstruktor ein I/O Serviceobjekt. Da timer einen Wecker darstellt, kann dem Konstruktor von boost::asio::steady_timer ein zweiter Parameter übergeben werden, der einen Zeitpunkt oder eine Zeitspanne angibt, nach deren Ablauf der Wecker klingeln soll. Im Beispiel 32.1 wird angegeben, dass der Wecker nach drei Sekunden klingeln soll. Die Zeit beginnt ab der timer-Definition zu laufen.

Anstatt eine blockierende Funktion aufzurufen, die nach drei Sekunden zurückkehrt, wenn der Wecker klingelt, kann mit Boost.Asio eine asynchrone Operation gestartet werden. Dazu wird die Methode async_wait() aufgerufen, der als einziger Parameter ein Handler übergeben wird. Ein Handler ist eine Funktion oder ein Funktionsobjekt, das aufgerufen wird, wenn die asynchrone Operation beendet wurde. Im obigen Beispiel wird eine Lambda-Funktion als Handler übergeben.

async_wait() kehrt sofort zurück. Anstatt drei Sekunden zu warten, bis der Wecker klingelt, wird nach drei Sekunden die Lambda-Funktion aufgerufen. Das Programm kann nach dem Aufruf von async_wait() etwas anderes tun als nur zu warten.

Eine Methode wie async_wait() wird als nicht-blockierend bezeichnet. Üblicherweise bieten I/O Objekte auch blockierende Methoden an. So kann zum Beispiel für boost::asio::steady_timer die blockierende Methode wait() aufgerufen werden. Da diese Methode blockiert, wird ihr keine Funktion übergeben. wait() kehrt zu einem bestimmten Zeitpunkt oder nach Ablauf einer Zeitspanne zurück.

Die letzte Anweisung in main() im Beispiel 32.1 ist der Aufruf von run() für das I/O Serviceobjekt. Dieser Methodenaufruf ist zwingend notwendig, da die betriebssystemeigenen Funktionen die Kontrolle übernehmen und nach drei Sekunden die Lambda-Funktion aufrufen müssen. Erinnern Sie sich, dass es die I/O Services im I/O Serviceobjekt sind, die asynchrone Operationen basierend auf Betriebssystemfunktionen implementieren.

Während async_wait() eine asynchrone Operation initiiert und sofort zurückkehrt, blockiert run(). Der Grund ist, dass viele Betriebssysteme asynchrone Operationen nur über eine blockierende Funktion unterstützen. Warum das in der Praxis üblicherweise kein Problem ist, sehen Sie anhand des folgenden Beispiels.

Beispiel 32.2. Zwei asynchrone Operationen mit 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();
}

Im Beispiel 32.2 werden zwei I/O Objekte vom Typ boost::asio::steady_timer verwendet. Das erste I/O Objekt repräsentiert einen Wecker, der nach drei Sekunden, das zweite einen Wecker, der nach vier Sekunden klingelt. Nach Ablauf der Zeitspannen werden die beiden Lambda-Funktionen aufgerufen, die an async_wait() übergeben wurden.

Auch in diesem Beispiel wird am Ende der Funktion main() die Methode run() für das einzige I/O Serviceobjekt aufgerufen. Durch diesen Methodenaufruf wird die Steuerung an Betriebssystemfunktionen übergeben, die die asynchrone Datenverarbeitung übernehmen. Mit ihrer Hilfe wird die erste Lambda-Funktion nach drei Sekunden und die zweite Lambda-Funktion nach vier Sekunden aufgerufen.

Es mag auf den ersten Blick verwundern, dass die asynchrone Datenverarbeitung den Aufruf einer blockierenden Methode erfordert. Das ist jedoch insofern kein Problem, als dass das Programm sowieso davor bewahrt werden muss, beendet zu werden. Würde run() nicht blockieren, würde die Funktion main() beendet werden – und damit das Programm. Für den Fall, dass nicht auf die Rückkehr des Methodenaufrufs gewartet werden und das Programm weiterlaufen soll, muss run() lediglich in einem neuen Thread aufgerufen werden.

Dass die Beispielprogramme dennoch beendet werden, liegt daran, dass run() zurückkehrt, wenn das I/O Serviceobjekt, für das diese Methode aufgerufen wurde, nichts mehr zu tun hat. Für die obigen Programme bedeutet dies, dass sie dann beendet werden, wenn alle Wecker geklingelt haben. Dann gibt es keine asynchronen Operationen mehr, die auf ihren Abschluss warten.