========================================== Konkurrierende Zugriffe auf eine Ressource ========================================== Bei unser bisherigen Parallelisierung nach dem Fork-And-Join-Pattern waren die einzelnen Threads vollkommen unabhängig voneinander. D.h. die einzelnen Threads haben weder direkt noch indirekt auf gemeinsame Datenstrukturen oder Ressourcen zugegriffen. Um diese Vorgehensweise konsequent durchzuziehen, haben wir insbesondere in jedem Thread einen neuen Pseudo-Zufallszahlen-Generator angelegt und mit Seed-Werten versorgt. Dies ist eine Operation, die nicht ganz billig ist. Während auf der Thales die Erzeugung einer pseudo-zufälligen, gleichverteilten Zahl unter Verwendung von `std::mt19937` im Durchschnitt etwa 8,4 Nanosekunden benötigt, kostet das Einrichten mindestens 9 Mikrosekunden, wobei das noch deutlich mehr wird, wenn etwas mehr Entropie als nur eine 32-Bit-Zahl gewünscht wird. Beispiele wie dieses führen zu der Frage, ob konkurrierende Zugriffe auf die gleiche Ressource wie beispielsweise einen Pseudo-Zufallszahlengenerator möglich sind. Die POSIX-Schnittstelle und auch die Thread-Schnittstelle von C++11 bieten für diesen Zweck sogenannte Mutex-Variablen (_mutex_ steht für _mutual exclusion_), mit deren Hilfe sichergestellt werden kann, dass nicht mehrere Threads zum gleichen Zeitpunkt auf eine Ressource zugreifen. Stattdessen bekommt immer nur ein Thread temporär den Zugriff. Andere Threads, die gleichzeitig zugreifen möchten, müssen dann solange warten, bis der andere Zugriff beendet ist. Die Verwendung einer Mutex-Variablen des Typs `std::mutex` aus `` ist recht einfach: * Die Methode `lock` blockiert den aufrufenden Thread bis die Mutex-Variable von keinem anderen Thread belegt ist und belegt sie dann. * Mit der Methode `unlock` wird die Mutex-Variable wieder freigegeben, so dass andere Threads zugreifen können. Der Zugriff auf einen Pseudo-Zufallszahlengenerator ließe sich dann so verpacken: ---- CODE (type=cpp) ---------------------------------------------------------- struct MyRandomGenerator { MyRandomGenerator() : mt(std::random_device()()), uniform(-100, 100) { } double gen() { mutex.lock(); double val = uniform(mt); mutex.unlock(); return val; } std::mutex mutex; std::mt19937 mt; std::uniform_real_distribution uniform; }; ------------------------------------------------------------------------------- Die Methode `gen` sichert sich hier zunächst mit `mutex.lock()` den exklusiven Zugriff, ruft dann einen Wert ab und gibt schließlich den exklusiven Zugriff wieder frei. Die Variablen der `MyRandomGenerator`-Klasse sind aber öffentlich zugänglich, d.h. jeder kann auch ohne die Verwendung der Methode `gen` auf `mt` oder gar `mutex` zugreifen. Bei den bisherigen Klassen hat uns dies bislang weniger gestört, bei Klassen wie diesen erscheint es sinnvoll, eine versehentliche Nutzung zu unterbinden, die an der Methode `gen` vorbeigeht. In C++ ist dies möglich, indem ein Teil der Deklarationen einer Klasse mit `private:` für den Zugriff von außen gesperrt wird. Auf private Daten können dann nur noch die Methoden der Klasse zugreifen: ---- CODE (type=cpp) ---------------------------------------------------------- struct MyRandomGenerator { MyRandomGenerator() : mt(std::random_device()()), uniform(-100, 100) { } double gen() { mutex.lock(); double val = uniform(mt); mutex.unlock(); return val; } private: std::mutex mutex; std::mt19937 mt; std::uniform_real_distribution uniform; }; ------------------------------------------------------------------------------- Die Korrektheit der Lösung hängt natürlich immer davon ab, dass das `mutex.unlock()` unter keinen Umständen versäumt wird, nachdem zuvor `mutex.lock()` aufgerufen wurde. Passiert dies doch, warten alle anderen Threads, die mit `gen()` einen Wert abholen wollen, endlos. Es kommt damit zu einem Endlos-Hänger, auch _deadlock_ genannt. Kann dies hier passieren, nachdem der Zugriff von außen unterbunden ist und sich so übersichtlich `mutex.lock()` mit `mutex.unlock()` paart? Ja, es ist theoretisch möglich, wenn es bei dem Aufruf von `uniform(mt)` zu einer Ausnahmenbehandlung kommt (_exception_) und im Zuge davon auch der Aufruf von `gen` automatisch abgebaut wird, ohne dass mehr die Gelegenheit besteht, `mutex.unlock()` aufzurufen. Die obige Lösung ist somit nicht _exception safe_. Es gibt in C++ nur einen Weg, so ein Problem zu lösen und der geht über Destruktoren. C++ garantiert, dass beim Abbau eines Blocks aufgrund einer Ausnahmenbehandlung die Destruktoren der dort deklarierten lokalen Variablen in jedem Fall aufgerufen werden. Entsprechend muss der Aufruf von `mutex.unlock()` einem Destruktor überlassen werden. Wir begegnen hier wieder dem RAII-Prinzip (_resource acquisition is initialization_), bei dem ein Objekt die Verantwortung für die Verwendung der Mutex-Variable übernimmt, die * bei der Initialisierung `mutex.lock()` aufruft und * bei dem Abbau, also beim Aufruf des Destruktors `mutex.unlock()` aufruft. Aufgabe ======= Schreiben Sie eine einfache RAII-Klasse wie beschrieben für Mutex-Variablen, bauen Sie diese in `MyRandomGenerator` ein und testen Sie dies, indem Sie die vor einer Woche entstandene Fassung von _random_init9.cpp_ ausbauen. Die Vorlesungsbibliothek steht auf unseren Rechnern unter _/home/numerik/pub/hpc/session15_ zur Verfügung. Die Header können entsprechend mit `-I/home/numerik/pub/hpc/session15` eingebunden werden. :import:session13/random_init8.cpp :navigate: up -> doc:index next -> doc:session15/page02