================================== Verbesserung der ThreadPool-Klasse ================================== Zuletzt haben wir gesehen, wie Jobs eines Thread-Pools in `std::packaged_task`-Objekte verpackt werden können und die `submit`-Methode ein `std::future`-Objekt zurückliefert, das es erlaubt, sich mit der Beendigung des Jobs zu synchronisieren und ein Ergebnis abzuholen. In dieser zuletzt vorgestellten Fassung musste der Return-Typ für den gesamten Thread-Pool als Template-Parameter festgelegt werden, d.h. unterschiedliche Jobs konnten nicht unterschiedliche Return-Typen haben. Es ist möglich, diesen Template-Parameter zu entfernen und stattdessen aus der `submit`-Methode eine Template-Methode zu machen, die als Jobs beliebige parameterlose Funktionstypen akzeptiert mit der Konsequenz, dass unterschiedliche Jobs auch unterschiedliche Return-Typen haben können. Das ist möglich, weil wir das `std::future`-Objekt sofort zurückgeben und alles andere vom Return-Typ unabhängig ist. Das Problem war nur, dass der Return-Typ als Template-Parameter eines Jobs, d.h. von `std::packaged_task` erhalten blieb: ---- CODE (type=cpp) ---------------------------------------------------------- using Job = std::packaged_task; std::list jobs; ------------------------------------------------------------------------------- Interessant ist hier aber, dass der Aufruf eines `std::packaged_task`-Objekts parameterlos ist und nur `void` zurückliefert. Somit liegt die Idee nahe, die `std::packaged_task`-Objekte in `std::function`-Objekte zu verpacken. Dies hätte den Vorteil, dass die Datenstruktur unabhängig von den Return-Typen ist und somit der Template-Parameter von `ThreadPool` entfallen kann. Leider lässt sich ein `std::packaged_task`-Objekt nicht ohne weiteres in ein `std::function`-Objekt verpacken, da `std::function` darauf besteht, dass das Funktionsobjekt per Kopie konstruiert werden kann (_copy constructible_), die `std::packaged_task`-Objekte jedoch nicht per Kopie, sondern nur per Verschieben konstruiert werden können (_move constructible_). Das liegt daran, dass zu einem `std::packaged_task`-Objekt auch ein `std::promise`-Objekt gehört und dieses nicht kopierbar ist, sondern auch nur Verschiebungen unterstützt. So ein Dilemma lässt sich lösen, indem ein solches Objekt hinter einen Zeiger versteckt wird, da Zeiger beliebig kopiert werden können. Dadurch entsteht aber das Problem, wann das Objekt hinter dem Zeiger freigegeben werden kann. Hierfür gibt es in C++ glücklicherweise sogenannte intelligente Zeiger (_smart pointers_), die in der Lage sind, die Zahl der Zeigerkopien zu zählen und das Objekt freizugeben, wenn die Zahl auf 0 sinkt. Diese Zeiger setzen eine Verallgemeinerung des RAII-Prinzips um, bei dem das Aufräumen verzögert wird, bis die letzte Kopie abgebaut wird. Der entsprechende Datentyp ist `std::shared_ptr` aus ``. Die Kombination aus dem `new`-Operator und das Verpacken in ein `std::shared_ptr`-Objekt steht mit der Template-Funktion `std::make_shared` zur Verfügung. In der jetzt zur Verfügung gestellten Fassung des Thread-Pools sieht die Liste der Jobs so aus (hier nennt sie sich `tasks`): ---- CODE (type=cpp) ---------------------------------------------------------- std::list> tasks; ------------------------------------------------------------------------------- Und `submit` kann dann so umgesetzt werden: ---- CODE (type=cpp) ---------------------------------------------------------- template auto submit(F&& task_function) -> std::future { using T = decltype(task_function()); auto task = std::make_shared>( std::forward(task_function)); std::future result = task->get_future(); std::lock_guard lock(mutex); tasks.push_back([task]() { (*task)(); } ); cv.notify_one(); return result; } ------------------------------------------------------------------------------- Sie finden diese Fassung in der neuen Version der Vorlesungsbibliothek unter dem Verzeichnis _/home/numerik/pub/hpc/session18_ auf unseren Rechnern. Der ThreadPool steht über `` zur Verfügung, der bislang verwendete `RandomEnginePool` ist über `` zu finden. Aufgaben ======== * Parallelisieren Sie die pseudo-zufällige Initialisierung einer Matrix mit der neuen ThreadPool-Klasse. * Erweitern Sie Ihr Programm dahingehend, dass nach der Fertigstellung der Initialisierung die Absolutbeträge der Matrixelemente aufsummiert werden. In `hpc/matvec/asum.h` steht eine entsprechende Funktion zur Verfügung, die parallelisiert zu verwenden ist. Hinweis: Der Return-Typ von `asum` ist nicht in jedem Fall der Element-Typ der Matrix. Warum? Wenn Sie die knifflige Konstruktion des Return-Typs von `asum` nicht kopieren wollen, dann empfiehlt sich die Verwendung des `auto`-Tricks in Verbindung mit `decltype`: Bei einer Funktion kann der Return-Typ mit `auto` spezifiziert werden, dann ist der Return-Typ hinter den Parametern nach dem Symbol `->` anzugeben. An dieser Stelle kann `decltype(...)` verwendet werden, wobei `decltype` den Typ des übergebenen Ausdrucks liefert. Es ist somit folgendes Konstrukt möglich: ---- CODE (type=cpp) ------------------------------------------------------- template auto asum(MA& A, /* ... */) -> decltype(hpc::matvec::asum(A)) { /* ... */ } ---------------------------------------------------------------------------- Auf diese Weise wird einfach der passende Return-Typ von `asum` übernommen. Vorlage ======= Als Vorlage kann die Fassung aus der 16. Session dienen: :import:session16/random_init13.cpp :navigate: up -> doc:index next -> doc:session18/page02