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

Das Problem der in Listing E gezeigten Lösung besteht in der Tatsache, dass falls 100 Threads gleichzeitig zu schreiben versuchen, dies nur jeweils einem gelingt, während die anderen auf den Lock warten. Je nach Art des zugehörigen Streams kann die Ausführung von m_underlyingLog << str sehr lange dauern, wobei alle anderen Threads in Wartestellung versetzt werden. Dies kann rasch zu Stauungen führen.

In der Funktion internal_thread_safe_log::write_message werden die Mitteilungen an eine Warteschlange angehängt. Ein spezieller Thread liest aus dieser Warteschlange und schreibt die Mitteilungen in den zugehörigen Stream. Die Funktion internal_thread_safe_log::write_message enthält nun lediglich zusätzlich einen Pointer auf eine Warteschlange, was fast gar keine Zeit erfordert, jedoch Engpässe vermeidet. Listing G zeigt die verbesserte Lösung.

Hier der genaue Unterschied zu der vorherigen Version:

  • internal_thread_safe_log besitzt ein Writer-Objekt.
  • Jedes Writer-Objekt (thread_safe_log_writer) besitzt seinen eigenen speziellen Thread, der die Mitteilungen in den zugehörigen Stream U schreibt.
  • Jedes Writer-Objekt muss mithilfe eines Thread-Managers seinen eigenen Thread erstellen.

Es ist Aufgabe des Thread-Managers, plattformabhängige Threading-Vorgänge abzukoppeln (beispielsweise das Erstellen eines neuen Threads). Auf den Thread-Manager wird später noch detaillierter eingegangen, für den Moment ist der aktuelle Thread-Manager jedenfalls win32_thread_manager. Die endgültige Lösung wird auch andere Thread-Manager zulassen.

Lösung 3

Je nach der Anzahl der Protokolle und/oder Threads, die die Anwendung verwenden soll, könnte die Lösung 2 nur wenig geeignet sein. Mehrere Protokolle könnten einen einzelnen Thread gemeinsam nutzen, der alle Protokolle schreibt. Lösung 3 bietet diese Funktionalität dank der folgenden Komponenten:

  • Ein Writer-Thread.
  • Mehrere Protokolle, die sich selbst beim Writer-Thread registrieren.
  • Jedes Protokoll innerhalb des Writer-Threads besitzt eine bestimmte Priorität. (Der Konstruktor des Protokolls enthält die Protokoll-Priorität als gesonderten Parameter.) Ausgehend von dieser Priorität zieht der Writer-Thread beim Schreiben bestimmte Protokolle gegenüber anderen vor.

Als Beispiel hierzu drei Protokolle: Protokoll 1 hat die Priorität 6, Protokoll 2 hat die Priorität 3 und Protokoll 3 hat die Priorität 1. (Die Prioritäten addieren sich also auf 10 = 6 + 3 + 1.) Im Writer-Thread erfolgen von 10 Schreibvorgängen sechs in Protokoll 1, drei in Protokoll 2 und einer in Protokoll 3. Wenn ein Schreibvorgang in ein Protokoll versucht wird, in dessen Warteschlange sich keine Mitteilung befindet, wird der Schreibvorgang ignoriert.

Der Test wurde wie in Listing H abgeändert und enthält nun die folgenden Funktionen:

  • Der neue Test enthält 10 Protokolle: out0.txt, out1.txt, …out9.txt.
  • Das Protokoll bei hat eine Priorität von 10 * (idx + 1)^2. Zu bemerken ist hierbei, dass das Protokoll out3.txt eine Priorität von 10 * 4 * 4 = 160 aufweist.
  • 20 Threads schreiben in out4.txt.

Beim Ausführen von Listing H ist zu beachten, dass die Protokolle mit höheren Zahlen aufgrund ihrer Priorität ein wenig schneller als die anderen gefüllt werden.

Lösung 4

Lösung 4 ermöglicht die Auswahl zwischen Lösung 2 und Lösung 3, je nach den jeweiligen Anwendungsanforderungen. Zum Wechsel von einer Lösung zur anderen müssen nur ein oder zwei Codezeilen in den get_log()-Funktionen geändert werden. Für eine solche Multiformat-Lösung sind die folgenden Komponenten erforderlich:

  • Zwei internal_thread_safe_log-Klassen: internal_thread_safe_log_ownthread, die einem einzelnen, eigenen Protokoll entspricht, und internal_thread_safe_log_sharethread, die von mehreren Protokollen gemeinsam verwendet wird.
  • internal_thread_safe_log_sharethread erfordert einen entsprechenden thread_safe_log_writer_sharethread, den Thread, den die Protokolle gemeinsam nutzen.

Listing I zeigt, wie einfach der Wechsel von internal_thread_safe_log_ownthread zu internal_thread_safe_log_sharethread und umgekehrt ist. Listing J stellt die gesamte Implementierung von Lösung 4 dar.

Listing J enthält die folgenden zusätzlichen Funktionen:

  • Die Klassen internal_thread_safe_log akzeptieren nun einen zusätzlichen Template-Parameter, den thread_manager, der zum Entkoppeln plattformabhängiger Threading-Vorgänge dient, wie unten erläutert.
  • Listing J benötigt das für Listing F erforderliche CriticalSection.h nicht, da CriticalSection.h in thread_manager verkapselt ist.
  • Bei jedem Leeren des Streams wird eine neue Mitteilung gestartet, so dass die jeweils letzte Mitteilung nie geleert wird. Daher benötigt die letzte Mitteilung eine gesonderte Verarbeitung. Der message_handler_log.h wurde geändert wie in Listing K gezeigt, um die Verarbeitung der letzten Mitteilung zu ermöglichen.

Das Auslassen des Leerens führt sehr wahrscheinlich zu Fehlern, wie Listing L zeigt. Daher wird der Leerungsvorgang vor dem Löschen jeder temporären Variablen zu einer Bedingung gemacht, wie in Listing M dargestellt. Die Implementierung dieser Bedingung ist ganz einfach: siehe thread_safe_log::on_last_message.

Im Destruktor thread_safe_log ist die Handhabung ungültiger Referenzen ausgeschlossen. Die Verwendung von temporären Variablen bringt jedoch ein spezielles Problem mit sich: die Nutzung dieser temporären Variablen nach deren Löschen ([temp-destructed]).

Der Test hat sich ebenfalls ein wenig verändert:

  • Es sind die 10 Protokolle (Protokolle 0-9) aus dem letzten Test vorhanden (mit gemeinsamer Thread-Nutzung).
  • Außerdem sind 10 Protokolle (Protokolle 10-19) jeweils mit eigenem Thread vorhanden.
  • 10 Threads schreiben in ein bestimmtes Protokoll (die Threads 4, 24, 44, … 184 schreiben in das vierte Protokoll).

Zuletzt sollen noch die Thread-Manager näher betrachtet werden. Ein Thread-Manager ist eine Klasse, die vorgibt, wie bestimmte Threading-Vorgänge zu handhaben sind. Er muss die folgenden Komponenten liefern:

  • thread_obj_base: Diese Klasse sollte beim Erstellen eines Threads als Argument weitergegeben werden; ihr überladener operator() wird auf dem anderen Thread ausgeführt.
  • sleep( nMillisecs): Der aktuelle Thread schläft für n Millisekunden.
  • create_thread( thread_obj_base & obj): Dies erstellt einen Thread und führt obj.operator() an ihm aus.
  • Klassen critical_section und auto_lock_unlock: Sie verhalten sich wie CCriticalSection und CAutoLockUnlock, wie in Listing F dargestellt.

Es werden zwei Thread-Manager bereitgestellt:

  • win32_thread_manager: Dies ist der Thread-Manager für Win32-Anwendungen.
  • boost_thread_manager: Dies ist der Thread-Manager zur Verwendung von Boost-Threads ([boost]).

Im Code kann über #define festgelegt werden, dass USE_WIN32_THREAD_MANAGER standardmäßig den ersteren Thread-Manager verwendet und USE_BOOST_THREAD_MANAGER den letzteren Thread-Manager. Man kann auch einen eigenen Thread-Manager bereitstellen und über #define DEFAULT_THREAD_MANAGER als your_threading_manager_class festlegen.

Nun ist Listing J auszuführen (wohlgemerkt, dazu ist der letzte message_handler_log.h erforderlich – Listing K). Dabei fällt auf, dass Listing J eine ganze Menge Klassen enthält. Aus diesem Grund wurden sie auf mehrere Dateien verteilt.

Komplex, aber machbar

Durch Nutzung der Klassen thread_safe_log und internal_thread_safe_log_* kann man die Protokollierung in gewohnter Weise durchführen und die Vorteile der Thread-sicherheit nutzen, ohne wesentliche Abstriche in puncto Effizienz machen zu müssen. Vorhandene Codes sowie Utility-Funktionen und -Klassen mit Nutzung von STL-Streams können so auf thread-sichere Weise eingesetzt werden. Auch das Refactoring bestehender Anwendungen ist problemlos möglich. Thread-sicherheit ist ein komplexes Thema, das sich jedoch mit ein wenig Aufwand meistern lässt und zahlreiche Vorteile bietet.

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

Kommentare sind bei diesem Artikel deaktiviert.