SSL/TLS-Netzwerkprogrammierung mit Boost.Asio, Teil 1: Grundlagen

Seite 3: Proactor Design Pattern

Inhaltsverzeichnis

Boost.Asio nutzt für die Verarbeitung dieser nebenläufigen Operationen keine Threads. Stattdessen implementiert es das "Proactor Design Pattern", wie im Text "Proactor" und im Buch von Douglas C. Schmidt und anderen "Pattern Oriented Software Architecture, Volume 2" (Wiley, 2000) beschrieben. Abbildung 1 gibt die Implementierung in Boost.Asio als UML-Klassendiagramm wieder. Der Initiator ist die Anwendung selbst. Sie stellt über den Asynchronous Operation Processor asynchrone Operationen ein und übergibt den erzeugten Completion Handler. Sobald der Asynchronous Operation Processor die Operation ausgeführt hat, stellt er das Ergebnis als Event in die Warteschlange, die in dem Fall Completion Event Queue heißt. Das io_service-Objekt nimmt die Rolle des Proactors wahr. Es nutzt den sogenannten Asynchronous Event Demuxer, der die eingetroffenen Ereignisse aus der Completion Event Queue abruft. Anschließend führt der Proactor, also das io_service-Objekt, den Completion Handler aus.

Der Aufbau des Proactor-Pattern in Boost.Asio, das ohne Threads auskommt (Abb.1)

Dabei kommt das System ohne das Erzeugen von Threads oder gar schwergewichtigen Prozessen aus. Das hat nicht nur Vorteile: Benötigt ein Handler lange für die Verarbeitung seiner Ergebnisse, blockiert das ganze System, wie ein Beispiel im nächsten Teil der Artikelreihe praktisch aufzeigen wird. Daher ist es sinnvoll, zeitintensive Verarbeitung aus dem Completion Handler eigenverantwortlich in Threads auszulagern. Das stellt sicher, dass io_service immer ansprechbar bleibt und auf Ereignisse rechtzeitig reagieren kann.

Die asynchrone Verarbeitung hat noch eine andere wichtige Konsequenz: Die Fehlerbehandlung ist mittels Exception-Handling nicht möglich, denn es gibt keine Methode, die Exceptions abfängt.

Zur Veranschaulichung soll folgendes Beispiel dienen: Die Applikation übergibt eine asynchrone Operation. io_service wertet das Ereignis aus. Im Fehlerfall wäre eine Exception wirkungslos. Sie müsste entweder innerhalb von io_service abgefangen werden oder außerhalb unter Terminierung der Operationsverarbeitung. Im ersten Fall könnte die Applikation keinerlei Fehlerbehandlung beisteuern, geschweige denn auf Fehler reagieren. Im zweiten Fall würde die Methode run() verlassen und damit die asynchrone Verarbeitung abgebrochen.

Daher erfolgt die Fehlerbehandlung über einen dem Handler mitgegebenen Statusparameter. Mit seiner Hilfe kann die Applikation dann aufgrund des Parameters entsprechend reagieren.

Wie bereits erwähnt, genügt in der Regel ein io_service pro Applikation. Die Methode run() des Objekts ist Thread-sicher und kann somit auch gefahrlos in mehreren Threads aufgerufen werden. Die einzelnen Instanzen von run() teilen sich eine gemeinsame -- ebenfalls Thread-sichere -- Event-Queue, da sie dem gleichen io_service-Objekt angehören. Damit können Entwickler die Verarbeitung von Ereignissen auf mehrere Threads verteilen und somit parallelisieren.

Ein Parallelisieren des Zugriffs auf einen Port durch mehrere io_service-Instanzen funktioniert nicht: Entwickler dürfen ein Socket-Objekt, das an eine Adresse und einen Port gebunden ist, nur einem einzigen io_service zuordnen. Weitere Dienstinstanzen können dasselbe Socket und damit auch die betreffende Adressen-Port-Kombination nicht bedienen. Mehrere io_service-Objekte sind also lediglich zum Trennen verschiedener Datenquellen wie weiterer Sockets, Zugriff auf Schnittstellen oder Dateien sinnvoll. Beispielsweise können Entwickler damit ein Benutzer- von einem Administrator-Interface logisch trennen.

Auch bei Boost.Asio gelten die allgemeinen Tabus der Netzwerkprogrammierung. So erzeugt beispielsweise ein paralleler Zugriff auf ein Socket aus unterschiedlichen Threads kein vorhersagbares Ergebnis. Die I/O-Objekte sind ebenso wenig Thread-sicher wie die betriebssystemspezifischen Pendants, die sie kapseln.