=================================== Fork-and-Join-Pattern mit Pipelines [TOC] =================================== Wenn wir das Fork-and-Join-Pattern zur Parallelisierung nutzen, müssen insbesondere folgende drei Teilprobleme gelöst werden: * Wie werden die Aufgaben aufgeteilt und an die einzelnen Prozesse kommuniziert? * Wie werden die Ergebnisse zu den Teilaufgaben an den Hauptprozess übermittelt? * Wie erfolgt die Synchronisierung? Charakteristisch für Fork-and-Join ist, dass eine Aufgabenverteilung nur einmal zu Beginn stattfindet. Dies ist somit in Verbindung mit _fork_ einfach zu lösen, weil die Aufteilung der Aufgaben vor dem _fork_ stattfinden kann und die benötigten Daten anschließend vererbt werden. Im Grunde muss jeder Prozess danach nur seinen Prozess-Index kennen. Dieser Index kann zum Beispiel für ein Array genutzt werden mit weiteren Daten. Für die Übermittlung der Ergebnisse gibt es bei unabhängigen Prozessen prinzipiell zwei Möglichkeiten: * Wir arbeiten mit einem gemeinsamen Speicherbereich, der mit _mmap_ angelegt worden ist und der Option `MAP_SHARED` über _fork_ hinweg vererbt werden kann. Der Nachteil ist hier, dass dieser Speicherbereich in der Größe begrenzt ist. Wenn die Ergebnisse im Umfang nicht offensichtlich beschränkt ist, lässt sich das nicht ohne ein aufwendiges Protokoll lösen, bei dem die Ergebnisse „häppchenweise“ geliefert werden und deswegen eine Synchronisierung auch zwischendrin stattfinden muss. * Wir nutzen Pipelines zu jedem der $n$ Worker-Prozesse, bei der die Worker das schreibende Ende haben (um ihr Ergebnis hineinzuschreiben) und der Hauptprozess das lesende Ende. Der Vorteil liegt in der fehlenden Beschränkung des Umfangs. Nachteilhaft ist hier, dass Worker-Prozesse unter Umständen schlafen gelegt werden, wenn der Hauptprozess nicht rechtzeitig die Ergebnisse bei einer gefüllten Pipeline wegliest. Aufgabe ======= Gegeben seien folgende Datentypen: ---- CODE (type=c) ------------------------------------------------------------ /* function to be executed by a worker, where id is an integer in [0, # workers) and fd the writing end of the pipeline */ typedef void (*JobHandler)(unsigned int id, int fd); typedef struct Worker { pid_t pid; int fd; /* reading end of the pipeline */ } Worker; ------------------------------------------------------------------------------- _JobHandler_ ist ein Funktionszeiger auf eine Funktion, die von einem Worker-Prozess auszuführen ist. Der erste Parameter spezifiziert den Prozess-Index und der zweite Parameter liefert den Datendeskriptor zum schreibenden Ende der Pipeline, die mit dem Hauptprozess verbunden ist. Zu entwickeln ist folgende Funktion, die eine gegebene Zahl von Worker-Prozessen erzeugt und für diese jeweils _handler_ aufruft: ---- CODE (type=c) ------------------------------------------------------------ bool spawn_workers(Worker workers[], unsigned int number_of_workers, JobHandler handler); ------------------------------------------------------------------------------- Hierbei sind im Array _workers_ die lesenden Enden und die Prozess-IDs der Worker zu hinterlassen. Im Erfolgsfalle ist _true_ zurückzuliefern, ansonsten _false_. Bitte testen Sie Ihre Lösung nicht auf unseren Servern wie etwa der Theon oder der Thales. Vorlage ======= :import: session04/fork-and-join0.c [fold] Beachten Sie, dass Sie beim Übersetzen die Bibliotheken „-lafb -lowfat“ angeben müssen. :navigate: up -> doc:index next -> doc:session04/page02