Nutzung von STL-Streams für einfache und thread-sichere Protokollierung in C++

Bekanntermaßen schreibt man wie folgt mithilfe des Operators << in einen Stream:


std::cout << val1 << val2 << …etc.

Intern wird alles, was man schreibt, in einem Stream-Puffer gespeichert und an die gewünschte Stelle geschrieben, wenn der Programmierer die Member-Funktion flush() aufruft oder der Puffer-Speicher voll ist und daher automatisch geleert wird.

Die Anweisung zur Leerung an einen Stream ist ganz einfach: Es genügt, std::endl in den Stream zu schreiben. Dies entspricht dem Schreiben von n in den Stream und dem anschließenden Leeren des Streams.

Zur Vereinfachung wird das Prinzip angewandt, dass beim Leeren eines Streams die aktuelle Mitteilung endet und eine neue Mitteilung beginnt, wie im Code unten zu sehen:


Um das Schreiben von Mitteilungen thread-sicher zu machen, sind die folgenden fünf Schritte auszuführen:

  1. Erstellen eines zugehörigen Streams U, zu dem ein thread-sicherer Zugang bestehen soll.
  2. Erstellen eines Streams S mit einer Referenz auf U.
  3. Sicherstellen, dass jeder Thread seinen eigenen Stream S besitzt
  4. Beim Schreiben in den Stream S müssen die Daten bis zur Leerung intern in einem Puffer gespeichert werden.
  5. Wenn der Stream S geleert wird (die aktuelle Mitteilung endet und eine neue Mitteilung beginnt), sollte er seinen Puffer in einer thread-sicheren Weise in den zugehörigen Stream U schreiben, und anschließend sollte Stream S seinen Puffer leeren.

Der unten aufgeführte Code zeigt diese Schritte:


Das Monitoring der Leerung ist nicht so einfach, wie es scheint. Dazu muss die Funktion sync() des Puffer-Speichers des Streams aufgehoben werden. Listing C zeigt die Klasse basic_message_handler_log, in der alle diese Details verborgen sind. Man kann jedoch auch einfach eine eigene Protokoll-Klasse erstellen und die Funktion on_new_message überschreiben, die nach dem Schreiben einer neuen Mitteilung aufgerufen wird.

Der Trick mit den temporären Variablen

Nochmals zusammengefasst: Jeder Thread sollte seinen eigenen Stream S besitzen, wobei S eine Referenz auf U enthält (der einen thread-sicheren Zugang benötigt). Die einfachste Methode, jeden Thread mit einem eigenen Stream S zu versehen, liegt in der kontinuierlichen Verwendung temporärer Variablen. Diese sind grundsätzlich thread-sicher, da nur der jeweils zur Verwendung der Variablen eingesetzte Thread auf sie zugreift. Der Trick besteht in der Verwendung einer Funktion get_log, die intern eine statische Variable besitzt (den zugehörigen Stream U) und eine temporäre Variable (den Stream S) ausgibt, wie in Listing D dargestellt.

Die in Listing D gezeigte Methode ist nicht nur einfacher als die meisten alternativen Verfahren, sie ist auch am effizientesten. Ein Locking findet nur beim Leeren des Streams statt.

Lösung 1

Die Klasse thread_safe_log ist von basic_message_handler_log abgeleitet (wie in Listing C gezeigt) und überschreibt on_new_message, wobei in thread-sicherer Weise in das zugehörige Protokoll geschrieben wird. Dabei stellt sich die Frage, wo das kritische Section-Objekt untergebracht werden soll. Um thread_safe_log zu entkoppeln, muss eine weitere Klasse erstellt werden – internal_thread_safe_log –, die das Threading übernimmt.

Eine Funktion get_log sieht wie folgt aus:


Wichtig sind hierbei die Klassen internal_thread_safe_log und thread_safe_log, auf sie wird in diesem Artikel häufig Bezug genommen.

Listing E zeigt die oben genannten Methoden. Dabei ist zu beachten, dass der Code in Listing F zur korrekten Ausführung die in Listing F enthaltene Datei CriticalSection.h benötigt.

Es lässt sich nun Folgendes feststellen:

  • Sowohl internal_thread_safe_log als auch thread_safe_log sind Typedefs ihrer Gegenstücke der Form basic_*. Dies stellt eine gängige Praxis dar, da z.B. std::ostream ein Typedef von std::basic_ostream ist und std::streambuf ein Typedef von std::basic_streambuf.
  • Auf jeden Zugriff auf get_log() folgt .ts(). Dies ist notwendig, da für bestimmte Stream-Operationen der Stream ein Lvalue ist. Kurz gesagt, kann ein Lvalue links von operator= stehen. Eine temporäre Variable ist ein Rvalue und muss in einen Lvalue umgewandelt werden.
  • Die temporäre Variable Stream S erhält bei ihrem Erstellen durch copy_state_to ihren Status von Stream U, wobei bei ihrem Löschen Stream S diesen Status zurück in U kopiert. Man kann sich den Status eines Streams als Information vorstellen, die vorgibt, wie bestimmte Daten geschrieben werden, beispielsweise das Füllzeichen, Locale oder andere Formatierungsangaben.

Wenn man diese Punkte nicht beachtet, kann dies schwerwiegende Folgen haben. Beispielsweise könnte U auf das deutsche Locale gesetzt sein (so dass 5.235 als 5,235 geschrieben wird). Würde S nun beim Drucken von Zahlen das standardmäßige Locale verwenden, würde die Ausgabe völlig falsch ausgelegt werden.

Listing E enthält außerdem einen Test. (Zu seiner Ausführung sind die Listings C und F erforderlich.) Der Test verwendet out.txt als zugehörigen Stream U. Er erstellt 200 konkurrierende Threads, wobei jeder der Threads 500 Mitteilungen folgender Art schreibt:

  • „writing double 5.23“ (erste Mitteilungsart)
  • „message from thread “ (zweite Mitteilungsart)

Wenn der Thread mit Index 10 die elfte Mitteilung schreibt, wird das Locale von U in Deutsch geändert. Alle nachfolgenden Mitteilungen schreiben dann „writing double 5,23“ statt „writing double 5.23“.

Themenseiten: Anwendungsentwicklung, Software

Fanden Sie diesen Artikel nützlich?
Content Loading ...
Whitepaper

Artikel empfehlen:

Neueste Kommentare 

Noch keine Kommentare zu Nutzung von STL-Streams für einfache und thread-sichere Protokollierung in C++

Kommentar hinzufügen

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *