C++ Threads für Klassenmethoden mit Parametern
In diesem Tutorial zeige ich wie man C++ Threads für Klassenmethoden mit Parametern erstellt. Mit dem seit C++ 11 verfügbaren standardisierten Thread Bibliothek nutzt man die Power von Multicore Systemen nun endlich vollständig aus.
C++ Threads für Klassenmethoden mit Parametern
Die Zeiten in denen man noch nicht standardisierte Bibliotheken wie OpenMP für die Erstellung von Threads in C++ verwenden musste sind schon länger vorbei. Der große Vorteil an std::thread ist dessen einfache Verwendbarkeit, der großen Verbreitung (hilfreich für Tipps und Tricks) und dessen Kompatibilität mit der restlichen STL.
Implementierung
Ich habe eine Klasse, die sich um das Laden von Dateien kümmert. Dort ist die gesamte Logik, vom Öffnen der Datei bis zum Auslesen der Daten, implementiert. So eine Klasse sieht beispielsweise wie folgt aus:
class MyClass { public: void loadFile(std::shared_ptr graph, std::string filename); }
Will man nun diese Klassenmethode über einen eigenen Thread auszuführen wird das wie folgt implementiert:
MyClass reader; std::thread thread(&MyClass::loadFile, &reader, graph, path + "Laender.sav"); thread.join();
Der Thread wird angelegt und als Parameter für den Konstruktor übergibt man den kompletten Pfad zur Funktion, also mit Namespace::Klassenname::Methodenname als Referenz und eine Referenz auf ein Objekt dieser Klasse. Danach folgen alle Argumente dieser Methode. Laut Dokumentation reicht ein Funktionspointer und die Argumente, das ist jedoch bei Klassenmethoden nicht ausreichend. Es gilt zu beachten, dass der Konstruktor den Thread nicht nur anlegt, er startet diesen auch sofort. Mit dem join Aufruf warten wir bis zur Beendigung des Threads. Je nachdem ob folgender Code parallel laufen kann oder nicht ist join auszuführen oder nicht. In meinem Fall lege ich eine gute Zahl von Treads an, die jeweils mit einem anderen filename aufgerufen werden. Danach warte ich auf erfolgreiches Abarbeiten.
Mutex
In meiner Implementierung bin ich unweigerlich auf ein lästiges Problem der Parallelisierung von Code gestoßen: gemeinsamer Zugriff auf Ressource. Der Thread lädt aus einer Datei im Dateisystem die Daten und verspeichert diese für das Programm effizient lesbar in einer Graphenstruktur. Diese habe ich mit der boost::graph Bibliothek realisiert. Rufe ich mein Programm nun 10 Mal auf, dann stürzt es 8 Mal ab. Es kommt zur gleichzeitigen Beanspruchung des Graphens um die Daten in den Hauptspeicher zu schieben. Die Lösung dafür ist ein Mutex. Ein solcher schützt einen bestimmten Abschnitt im Code davor von mehreren Threads gleichzeitig ausgeführt zu werden. Zweigt der erste laufende Thread in solch einen Bereich ab, dann ist dieser für alle anderen Threads bis zur Beendigung gesperrt, die anderen Threads müssen warten. Dank der Standardbibliothek ist die Implementierung extrem einfach:
class MyClass { public: void loadFile(std::shared_ptr graph, std::string filename); private: std::mutex mutex; }
Ich erweitere meine Klasse die für das Lesen und Verspeichern der Daten in den Graphen verantwortlich ist ein neues Member mutex. In der loadFile Methode rufe ich kurz vor Schluss vor dem Bereich in dem das neu erstellte Objekt in den Graphen eingefügt wird folgende Zeile Code auf:
// makes graph insertion thread safe std::lock_guard<std::mutex> lockguard(mutex);
Der lock_guard legt einen neuen Mutex an. Diese ist bis zum Ende der Methode aktiv. Seit dieser Erweiterung funktioniert das gleichzeitige Einlesen beliebig vieler Dateien durch Threads ohne Probleme.
Messung
In meinem Test beschleunigte sich das Laden von 11 Dateien in eine boost::graph Graphenstruktur um fast den Faktor 2. Anstatt von 36 Sekunden brauchte der Programmstart nur noch knapp 20 Sekunden. Getestet wurde auf einem i7 Dual-Core mit Hyperthreading. Auf einem System mit mehr Kernen würde sich das noch etwas beschleunigen, mit der passenden Hardware sind vermutlich 10 Sekunden realistisch. Die Daten beziehen sich übrigens auf die Debug Version, sollte jetzt jemand meinen C++ wäre nicht sehr performant. In der Release Version ist dieser Ladevorgang kaum messbar, so schnell findet dieser statt.
Fazit
Die Implementierung von Threads für Klassenmethoden mit Parametern ist im aktuellen C++ Standard denkbar einfach. Sogar das Problem mit gleichzeitigen Speicherzugriffen ist dank einem Mutex und dem lock_guard schnell gelöst. Dank paralleler Abarbeitung der Aufgaben läuft ein Programm wesentlich schneller. Ein C++ Programmierer sollte so viel wie möglich in Threads auslagern.